fix(db): support routine rename editing

This commit is contained in:
t8y2
2026-05-18 21:24:03 +08:00
parent 06b0c60a0e
commit da00405e10
10 changed files with 453 additions and 30 deletions
@@ -34,7 +34,12 @@ import type { ConnectionConfig, ObjectInfo, ObjectSourceKind } from "@/types/dat
import { isSchemaAware } from "@/lib/databaseCapabilities";
import { buildTableSelectSql, qualifiedTableName } from "@/lib/tableSelectSql";
import { useToast } from "@/composables/useToast";
import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import {
buildExecutableObjectSourceStatements,
buildRoutineRenameObjectSourceStatements,
objectSourceSaveExecutionMode,
supportsSourceBackedRoutineRename,
} from "@/lib/objectSourceEditor";
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/objectRenameSql";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
@@ -169,7 +174,10 @@ function canOpenSource(row: ObjectBrowserRow) {
}
function canRename(row: ObjectBrowserRow) {
return supportsObjectRename(props.connection.db_type, row.type);
return (
supportsObjectRename(props.connection.db_type, row.type) ||
supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)
);
}
function sourceTitle(row: ObjectBrowserRow | null) {
@@ -250,6 +258,9 @@ function renamePreviewSql() {
const row = renameTarget.value;
const newName = renameInput.value.trim();
if (!row || !newName || newName === row.name) return "";
if (supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)) {
return `-- Recreate ${row.type} from source, then drop the original object.`;
}
try {
return buildRenameObjectSql({
databaseType: props.connection.db_type,
@@ -270,14 +281,35 @@ async function confirmRename() {
renameError.value = "";
try {
const schema = row.schema || selectedSchema.value || props.database;
const sql = buildRenameObjectSql({
databaseType: props.connection.db_type,
objectType: row.type,
schema,
oldName: row.name,
newName,
});
await api.executeQuery(props.connection.id, props.database, sql, schema);
if (supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)) {
const source = await api.getObjectSource(
props.connection.id,
props.database,
schema,
row.name,
row.type as ObjectSourceKind,
);
const statements = buildRoutineRenameObjectSourceStatements({
databaseType: props.connection.db_type,
objectType: row.type as ObjectSourceKind,
schema,
name: row.name,
newName,
source: source.source,
});
for (const sql of statements) {
await api.executeQuery(props.connection.id, props.database, sql, schema);
}
} else {
const sql = buildRenameObjectSql({
databaseType: props.connection.db_type,
objectType: row.type,
schema,
oldName: row.name,
newName,
});
await api.executeQuery(props.connection.id, props.database, sql, schema);
}
toast(t("contextMenu.renameObjectSuccess", { oldName: row.name, newName }));
showRenameDialog.value = false;
if (sourceRow.value?.id === row.id) closeSource();
@@ -102,6 +102,7 @@ import {
uniqueDuckDbAttachedDatabaseName,
} from "@/lib/createDatabaseSql";
import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType } from "@/lib/objectRenameSql";
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
import { hexToRgba } from "@/lib/color";
import { focusSidebarRenameInput, shouldPreventRenameCloseAutoFocus } from "@/lib/sidebarRenameFocus";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
@@ -687,7 +688,11 @@ function nodeRenameObjectType(): RenameableObjectType | null {
const canRenameObject = computed(() => {
const objectType = nodeRenameObjectType();
return !!objectType && supportsObjectRename(currentDatabaseType(), objectType);
return (
!!objectType &&
(supportsObjectRename(currentDatabaseType(), objectType) ||
supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any))
);
});
function openRenameObjectDialog() {
@@ -700,6 +705,9 @@ function buildRenameObjectPreviewSql(): string {
const objectType = nodeRenameObjectType();
const newName = renameObjectName.value.trim();
if (!objectType || !newName || newName === props.node.label) return "";
if (supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any)) {
return `-- Recreate ${objectType} from source, then drop the original object.`;
}
try {
return buildRenameObjectSql({
databaseType: currentDatabaseType(),
@@ -720,15 +728,32 @@ async function confirmRenameObject() {
if (!objectType || !newName || newName === node.label || !node.connectionId || !node.database) return;
renameObjectError.value = "";
try {
const sql = buildRenameObjectSql({
databaseType: currentDatabaseType(),
objectType,
schema: node.schema,
oldName: node.label,
newName,
});
const dbType = currentDatabaseType();
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
if (supportsSourceBackedRoutineRename(dbType, objectType as any)) {
const schema = node.schema || node.database;
const source = await api.getObjectSource(node.connectionId, node.database, schema, node.label, objectType as any);
const statements = buildRoutineRenameObjectSourceStatements({
databaseType: dbType!,
objectType: objectType as any,
schema,
name: node.label,
newName,
source: source.source,
});
for (const sql of statements) {
await api.executeQuery(node.connectionId, node.database, sql, schema);
}
} else {
const sql = buildRenameObjectSql({
databaseType: dbType,
objectType,
schema: node.schema,
oldName: node.label,
newName,
});
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
}
toast(t("contextMenu.renameObjectSuccess", { oldName: node.label, newName }), 3000);
showRenameObjectDialog.value = false;
await refreshTableList(node);
@@ -98,6 +98,8 @@ export const TABLE_STRUCTURE_SUPPORTED_TYPES = new Set<DatabaseType>([
"sqlite",
"duckdb",
"sqlserver",
"oracle",
"dameng",
]);
export const CREATE_DATABASE_SUPPORTED_TYPES = new Set<DatabaseType>([
+4 -2
View File
@@ -49,7 +49,9 @@ export function supportsObjectRename(
): boolean {
if (!databaseType) return false;
if (databaseType === "sqlserver") return true;
if (objectType === "PROCEDURE" || objectType === "FUNCTION") return false;
if (objectType === "PROCEDURE" || objectType === "FUNCTION") {
return false;
}
if (databaseType === "sqlite") return objectType === "TABLE";
if (databaseType === "mysql" || databaseType === "goldendb") return objectType === "TABLE" || objectType === "VIEW";
if (postgresLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW";
@@ -79,7 +81,7 @@ export function buildRenameObjectSql(options: BuildRenameObjectSqlOptions): stri
postgresLikeRenameTypes.has(databaseType as DatabaseType) ||
oracleLikeRenameTypes.has(databaseType as DatabaseType)
) {
const keyword = objectType === "VIEW" ? "VIEW" : "TABLE";
const keyword = objectType;
return `ALTER ${keyword} ${qualifiedName(databaseType, schema, oldName)} RENAME TO ${quoteRenameIdentifier(databaseType, newName)};`;
}
+122 -2
View File
@@ -8,6 +8,10 @@ type BuildEditableObjectSourceSqlInput = {
source: string;
};
type BuildRoutineRenameObjectSourceInput = BuildEditableObjectSourceSqlInput & {
newName: string;
};
export type ObjectSourceSaveExecutionMode = "single" | "script";
const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
@@ -18,11 +22,17 @@ const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
"highgo",
"vastbase",
]);
const mysqlLikeRoutineRenameTypes = new Set<DatabaseType>(["mysql", "goldendb"]);
const oracleLikeRoutineRenameTypes = new Set<DatabaseType>(["oracle", "dameng"]);
function quotePostgresIdentifier(value: string) {
return `"${value.replaceAll('"', '""')}"`;
}
function quoteMysqlIdentifier(value: string) {
return `\`${value.replaceAll("`", "``")}\``;
}
function ensureSemicolon(sql: string) {
const trimmed = sql.trim();
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
@@ -35,6 +45,13 @@ function postgresQualifiedName(schema: string | null | undefined, name: string)
.join(".");
}
function mysqlQualifiedName(schema: string | null | undefined, name: string) {
return [schema, name]
.filter(Boolean)
.map((part) => quoteMysqlIdentifier(part as string))
.join(".");
}
function unquotePostgresIdentifier(value: string) {
const trimmed = value.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1).replaceAll('""', '"');
@@ -46,9 +63,20 @@ function splitQualifiedRoutineName(value: string) {
return parts.map(unquotePostgresIdentifier);
}
function unquoteMysqlIdentifier(value: string) {
const trimmed = value.trim();
if (trimmed.startsWith("`") && trimmed.endsWith("`")) return trimmed.slice(1, -1).replaceAll("``", "`");
return trimmed;
}
function splitMysqlQualifiedRoutineName(value: string) {
const parts = value.match(/`(?:``|[^`])+`|[A-Za-z_][\w$]*/g) ?? [];
return parts.map(unquoteMysqlIdentifier);
}
function routineDeclaration(source: string) {
const match = source.match(
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(FUNCTION|PROCEDURE)\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)\s*(\([^]*?\))?/i,
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(FUNCTION|PROCEDURE)\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)\s*(\([^]*?\))?/i,
);
if (!match) return null;
const nameParts = splitQualifiedRoutineName(match[2]);
@@ -61,14 +89,57 @@ function routineDeclaration(source: string) {
};
}
function replaceSqlRoutineDeclarationName(source: string, schema: string | null | undefined, newName: string) {
const match = source.match(
/^(\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)/i,
);
if (!match) return null;
const existingParts = splitQualifiedRoutineName(match[2]);
const schemaName = schema || (existingParts.length > 1 ? existingParts[0] : null);
const replacement = schemaName
? `${quotePostgresIdentifier(schemaName)}.${quotePostgresIdentifier(newName)}`
: quotePostgresIdentifier(newName);
return `${source.slice(0, match.index)}${match[1]}${replacement}${source.slice((match.index ?? 0) + match[0].length)}`;
}
function mysqlRoutineDeclaration(source: string) {
const match = source.match(
/^\s*CREATE\s+(?:DEFINER\s*=\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s+)?(FUNCTION|PROCEDURE)\s+((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)/i,
);
if (!match) return null;
const nameParts = splitMysqlQualifiedRoutineName(match[2]);
const name = nameParts[nameParts.length - 1];
if (!name) return null;
return {
kind: match[1].toUpperCase() as "FUNCTION" | "PROCEDURE",
name,
};
}
function replaceMysqlRoutineDeclarationName(source: string, newName: string) {
const match = source.match(
/^(\s*CREATE\s+(?:DEFINER\s*=\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)/i,
);
if (!match) return null;
return `${source.slice(0, match.index)}${match[1]}${quoteMysqlIdentifier(newName)}${source.slice((match.index ?? 0) + match[0].length)}`;
}
function routineNameChanged(sourceName: string, savedName: string) {
return sourceName.toLowerCase() !== savedName.toLowerCase();
}
function buildRoutineRenameCleanup(input: BuildEditableObjectSourceSqlInput, source: string) {
if (!postgresLikeRoutineRenameTypes.has(input.databaseType)) return null;
if (input.objectType !== "FUNCTION" && input.objectType !== "PROCEDURE") return null;
if (mysqlLikeRoutineRenameTypes.has(input.databaseType)) {
const declaration = mysqlRoutineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) return null;
if (!routineNameChanged(declaration.name, input.name)) return null;
return `DROP ${input.objectType} IF EXISTS ${mysqlQualifiedName(input.schema, input.name)};`;
}
if (!postgresLikeRoutineRenameTypes.has(input.databaseType)) return null;
const declaration = routineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) return null;
if (!routineNameChanged(declaration.name, input.name)) return null;
@@ -76,6 +147,55 @@ function buildRoutineRenameCleanup(input: BuildEditableObjectSourceSqlInput, sou
return `DROP ${input.objectType} IF EXISTS ${postgresQualifiedName(input.schema, input.name)}${declaration.signature};`;
}
export function supportsSourceBackedRoutineRename(
databaseType: DatabaseType | undefined,
objectType: ObjectSourceKind,
): boolean {
if (objectType !== "FUNCTION" && objectType !== "PROCEDURE") return false;
if (!databaseType || databaseType === "sqlserver") return false;
return (
mysqlLikeRoutineRenameTypes.has(databaseType) ||
postgresLikeRoutineRenameTypes.has(databaseType) ||
oracleLikeRoutineRenameTypes.has(databaseType)
);
}
export function buildRoutineRenameObjectSourceStatements(input: BuildRoutineRenameObjectSourceInput) {
if (!supportsSourceBackedRoutineRename(input.databaseType, input.objectType)) {
throw new Error(`Renaming ${input.objectType} from source is not supported for ${input.databaseType}.`);
}
const source = input.source.trim();
const declaration = mysqlLikeRoutineRenameTypes.has(input.databaseType)
? mysqlRoutineDeclaration(source)
: routineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) {
throw new Error(`Cannot find a CREATE ${input.objectType} declaration in the object source.`);
}
const renamedSource = mysqlLikeRoutineRenameTypes.has(input.databaseType)
? replaceMysqlRoutineDeclarationName(source, input.newName)
: replaceSqlRoutineDeclarationName(source, input.schema, input.newName);
if (!renamedSource) {
throw new Error(`Cannot rewrite the ${input.objectType} name in the object source.`);
}
if (oracleLikeRoutineRenameTypes.has(input.databaseType)) {
return [
ensureSemicolon(renamedSource),
`DROP ${input.objectType} ${postgresQualifiedName(input.schema, input.name)};`,
];
}
return buildExecutableObjectSourceStatements({
databaseType: input.databaseType,
objectType: input.objectType,
schema: input.schema,
name: input.name,
source: renamedSource,
});
}
export function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput) {
const source = input.source.trim();
if (input.databaseType === "sqlserver") {
@@ -45,8 +45,12 @@ function quoteIdent(databaseType: DatabaseType | undefined, name: string): strin
return `"${name.replace(/"/g, '""')}"`;
}
function isOracleLike(databaseType: DatabaseType | undefined): databaseType is "oracle" | "dameng" {
return databaseType === "oracle" || databaseType === "dameng";
}
function qualifiedTable(databaseType: DatabaseType | undefined, schema: string | undefined, tableName: string): string {
if ((databaseType === "postgres" || databaseType === "oracle" || databaseType === "sqlserver") && schema) {
if ((databaseType === "postgres" || isOracleLike(databaseType) || databaseType === "sqlserver") && schema) {
return `${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, tableName)}`;
}
return quoteIdent(databaseType, tableName);
@@ -67,7 +71,7 @@ function normalizeDefault(value: string | null | undefined): string {
function columnDefinition(databaseType: DatabaseType | undefined, column: EditableStructureColumn): string {
const parts = [quoteIdent(databaseType, column.name), column.dataType.trim()];
if (!column.isNullable) parts.push("NOT NULL");
if (!column.isNullable && !isOracleLike(databaseType)) parts.push("NOT NULL");
const defaultValue = normalizeDefault(column.defaultValue);
if (defaultValue) parts.push(`DEFAULT ${defaultValue}`);
if (databaseType === "mysql" && clean(column.comment)) {
@@ -102,8 +106,11 @@ function buildAddColumnSql(
column: EditableStructureColumn,
): string[] {
const addKeyword = databaseType === "sqlserver" ? "ADD" : "ADD COLUMN";
const statements = [`ALTER TABLE ${table} ${addKeyword} ${columnDefinition(databaseType, column)};`];
if (databaseType === "postgres" && clean(column.comment)) {
const definition = columnDefinition(databaseType, column);
const statements = isOracleLike(databaseType)
? [`ALTER TABLE ${table} ADD (${definition});`]
: [`ALTER TABLE ${table} ${addKeyword} ${definition};`];
if ((databaseType === "postgres" || isOracleLike(databaseType)) && clean(column.comment)) {
statements.push(
`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, column.name)} IS ${quoteString(clean(column.comment))};`,
);
@@ -111,6 +118,42 @@ function buildAddColumnSql(
return statements;
}
function buildOracleLikeExistingColumnSql(
databaseType: DatabaseType,
table: string,
column: EditableStructureColumn,
): string[] {
const original = column.original;
if (!original) return [];
const statements: string[] = [];
let currentName = original.name;
if (column.name !== original.name) {
statements.push(
`ALTER TABLE ${table} RENAME COLUMN ${quoteIdent(databaseType, original.name)} TO ${quoteIdent(databaseType, column.name)};`,
);
currentName = column.name;
}
if (column.dataType.trim() !== original.data_type.trim()) {
statements.push(
`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} ${column.dataType.trim()});`,
);
}
if (column.isNullable !== original.is_nullable) {
const nullability = column.isNullable ? "NULL" : "NOT NULL";
statements.push(`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} ${nullability});`);
}
if (normalizeDefault(column.defaultValue) !== originalDefault(column)) {
const defaultValue = normalizeDefault(column.defaultValue) || "NULL";
statements.push(`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} DEFAULT ${defaultValue});`);
}
if (clean(column.comment) !== originalComment(column)) {
const commentValue = clean(column.comment) ? quoteString(clean(column.comment)) : "NULL";
statements.push(`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, currentName)} IS ${commentValue};`);
}
return statements;
}
function buildMysqlExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
const originalName = column.original?.name ?? column.name;
const operation =
@@ -199,6 +242,8 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
statements.push(...buildMysqlExistingColumnSql(table, column));
} else if (databaseType === "postgres") {
statements.push(...buildPostgresExistingColumnSql(table, column));
} else if (isOracleLike(databaseType)) {
statements.push(...buildOracleLikeExistingColumnSql(databaseType, table, column));
} else if (databaseType === "sqlite") {
statements.push(...buildSqliteExistingColumnSql(table, column, warnings));
} else {
@@ -217,7 +262,7 @@ function buildDropIndexSql(
): string {
if (databaseType === "mysql") return `DROP INDEX ${quoteIdent(databaseType, indexName)} ON ${table};`;
if (databaseType === "sqlserver") return `DROP INDEX ${quoteIdent(databaseType, indexName)} ON ${table};`;
if ((databaseType === "postgres" || databaseType === "oracle") && schema) {
if ((databaseType === "postgres" || isOracleLike(databaseType)) && schema) {
return `DROP INDEX ${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, indexName)};`;
}
return `DROP INDEX ${quoteIdent(databaseType, indexName)};`;
@@ -344,7 +389,7 @@ export function buildCreateTableSql(options: BuildTableStructureChangeSqlOptions
statements.push(`CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n);`);
if (databaseType === "postgres") {
if (databaseType === "postgres" || isOracleLike(databaseType)) {
for (const col of activeColumns) {
if (clean(col.comment)) {
statements.push(