fix(objects): handle routine source renames

This commit is contained in:
t8y2 2026-05-18 12:14:19 +08:00
parent 82f78bc805
commit 4bb0e0308f
4 changed files with 129 additions and 17 deletions

View File

@ -34,7 +34,7 @@ import "@/i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import * as api from "@/lib/api";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { sqlFileTitleFromPath } from "@/lib/sqlFileOpen";
@ -284,17 +284,19 @@ async function saveActiveObjectSource(tab: NonNullable<typeof activeTab.value>)
if (!connection || !source) return;
try {
const sql = buildExecutableObjectSourceSql({
const statements = buildExecutableObjectSourceStatements({
databaseType: connection.db_type,
objectType: source.objectType,
schema: source.schema || tab.schema || tab.database,
name: source.name,
source: tab.sql,
});
if (objectSourceSaveExecutionMode(connection.db_type) === "single") {
await api.executeQuery(tab.connectionId, tab.database, sql, source.schema || tab.schema);
} else {
await api.executeScript(tab.connectionId, tab.database, sql, source.schema || tab.schema);
for (const sql of statements) {
if (objectSourceSaveExecutionMode(connection.db_type) === "single") {
await api.executeQuery(tab.connectionId, tab.database, sql, source.schema || tab.schema);
} else {
await api.executeScript(tab.connectionId, tab.database, sql, source.schema || tab.schema);
}
}
toast(t("objects.sourceSaved"), 2000);
} catch (e: any) {

View File

@ -35,7 +35,7 @@ import { isSchemaAware } from "@/lib/databaseCapabilities";
import { buildTableSelectSql, qualifiedTableName } from "@/lib/tableSelectSql";
import { normalizeDatabaseObjectName } from "@/lib/tableTree";
import { useToast } from "@/composables/useToast";
import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/objectRenameSql";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
@ -220,6 +220,8 @@ async function openSource(row: ObjectRow) {
row.type as ObjectSourceKind,
);
sourceContent.value = result.source;
sourceDraft.value = result.source;
sourceEditing.value = true;
} catch (e: any) {
sourceError.value = e?.message || String(e);
} finally {
@ -395,17 +397,19 @@ async function saveSource() {
sourceSaving.value = true;
sourceSaveError.value = "";
try {
const sql = buildExecutableObjectSourceSql({
const statements = buildExecutableObjectSourceStatements({
databaseType: props.connection.db_type,
objectType: row.type as ObjectSourceKind,
schema,
name: row.name,
source: sourceDraft.value,
});
if (objectSourceSaveExecutionMode(props.connection.db_type) === "single") {
await api.executeQuery(props.connection.id, props.database, sql, schema);
} else {
await api.executeScript(props.connection.id, props.database, sql, schema);
for (const sql of statements) {
if (objectSourceSaveExecutionMode(props.connection.db_type) === "single") {
await api.executeQuery(props.connection.id, props.database, sql, schema);
} else {
await api.executeScript(props.connection.id, props.database, sql, schema);
}
}
toast(t("objects.sourceSaved"));
sourceEditing.value = false;

View File

@ -10,6 +10,15 @@ type BuildEditableObjectSourceSqlInput = {
export type ObjectSourceSaveExecutionMode = "single" | "script";
const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
"postgres",
"redshift",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
]);
function quotePostgresIdentifier(value: string) {
return `"${value.replaceAll('"', '""')}"`;
}
@ -26,17 +35,64 @@ function postgresQualifiedName(schema: string | null | undefined, name: string)
.join(".");
}
export function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceSqlInput) {
function unquotePostgresIdentifier(value: string) {
const trimmed = value.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1).replaceAll('""', '"');
return trimmed;
}
function splitQualifiedRoutineName(value: string) {
const parts = value.match(/"(?:""|[^"])+"|[A-Za-z_][\w$]*/g) ?? [];
return parts.map(unquotePostgresIdentifier);
}
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,
);
if (!match) return null;
const nameParts = splitQualifiedRoutineName(match[2]);
const name = nameParts[nameParts.length - 1];
if (!name) return null;
return {
kind: match[1].toUpperCase() as "FUNCTION" | "PROCEDURE",
name,
signature: match[3]?.trim() ?? "",
};
}
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;
const declaration = routineDeclaration(source);
if (!declaration || declaration.kind !== input.objectType) return null;
if (!routineNameChanged(declaration.name, input.name)) return null;
return `DROP ${input.objectType} IF EXISTS ${postgresQualifiedName(input.schema, input.name)}${declaration.signature};`;
}
export function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput) {
const source = input.source.trim();
if (input.databaseType === "sqlserver") {
return source.replace(/^CREATE\s+(?:OR\s+ALTER\s+)?/i, "ALTER ");
return [source.replace(/^CREATE\s+(?:OR\s+ALTER\s+)?/i, "ALTER ")];
}
if ((input.databaseType === "postgres" || input.databaseType === "gaussdb") && input.objectType === "VIEW") {
return `CREATE OR REPLACE VIEW ${postgresQualifiedName(input.schema, input.name)} AS\n${ensureSemicolon(source)}`;
return [`CREATE OR REPLACE VIEW ${postgresQualifiedName(input.schema, input.name)} AS\n${ensureSemicolon(source)}`];
}
return ensureSemicolon(source);
const createStatement = ensureSemicolon(source);
const cleanup = buildRoutineRenameCleanup(input, source);
return cleanup ? [createStatement, cleanup] : [createStatement];
}
export function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceSqlInput) {
return buildExecutableObjectSourceStatements(input).join("\n");
}
export function objectSourceSaveExecutionMode(_databaseType: DatabaseType): ObjectSourceSaveExecutionMode {

View File

@ -1,6 +1,10 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "../../apps/desktop/src/lib/objectSourceEditor.ts";
import {
buildExecutableObjectSourceSql,
buildExecutableObjectSourceStatements,
objectSourceSaveExecutionMode,
} from "../../apps/desktop/src/lib/objectSourceEditor.ts";
test("SQL Server edited source saves as ALTER", () => {
const sql = buildExecutableObjectSourceSql({
@ -54,3 +58,49 @@ test("Postgres view body opens as CREATE OR REPLACE VIEW", () => {
assert.equal(sql, 'CREATE OR REPLACE VIEW "public"."active users" AS\nSELECT id, name FROM users WHERE active;');
});
test("Kingbase function rename creates the renamed routine and then drops the original routine", () => {
const statements = buildExecutableObjectSourceStatements({
databaseType: "kingbase",
objectType: "FUNCTION",
schema: "DLJPM",
name: "CONVERTSPECIALNAME",
source:
'CREATE OR REPLACE function "DLJPM"."CONVERTSPECIALNAME1" (SpName varchar2)\nRETURN VARCHAR2\nas\nbegin\nreturn SpName;\nend;',
});
assert.deepEqual(statements, [
'CREATE OR REPLACE function "DLJPM"."CONVERTSPECIALNAME1" (SpName varchar2)\nRETURN VARCHAR2\nas\nbegin\nreturn SpName;\nend;',
'DROP FUNCTION IF EXISTS "DLJPM"."CONVERTSPECIALNAME"(SpName varchar2);',
]);
});
test("Postgres procedure rename creates the renamed routine and then drops the original routine", () => {
const statements = buildExecutableObjectSourceStatements({
databaseType: "postgres",
objectType: "PROCEDURE",
schema: "public",
name: "refresh_cache",
source: 'CREATE OR REPLACE PROCEDURE "public"."refresh_cache_v2"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;',
});
assert.deepEqual(statements, [
'CREATE OR REPLACE PROCEDURE "public"."refresh_cache_v2"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;',
'DROP PROCEDURE IF EXISTS "public"."refresh_cache"(mode text);',
]);
});
test("object source SQL joins generated save statements for previews", () => {
const sql = buildExecutableObjectSourceSql({
databaseType: "postgres",
objectType: "PROCEDURE",
schema: "public",
name: "refresh_cache",
source: 'CREATE OR REPLACE PROCEDURE "public"."refresh_cache_v2"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;',
});
assert.equal(
sql,
'CREATE OR REPLACE PROCEDURE "public"."refresh_cache_v2"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;\nDROP PROCEDURE IF EXISTS "public"."refresh_cache"(mode text);',
);
});