fix(objects): handle routine source renames
This commit is contained in:
parent
82f78bc805
commit
4bb0e0308f
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue