diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue
index fc676dafa5..20c255eb6c 100644
--- a/apps/desktop/src/components/objects/ObjectBrowser.vue
+++ b/apps/desktop/src/components/objects/ObjectBrowser.vue
@@ -62,7 +62,7 @@ import { getTableMetadataCapabilities, type TableMetadataCapabilities } from "@/
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, supportsTruncateTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
import { useToast } from "@/composables/useToast";
-import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, executeObjectSourceSave, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
+import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, executeObjectSourceSave, formatObjectSourceSaveError, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
@@ -2037,9 +2037,9 @@ async function saveSource() {
sourceEditing.value = false;
sourceDraft.value = "";
await openSource(row);
- } catch (e: any) {
+ } catch (e: unknown) {
if (sidePanelGuard.isStale(epoch)) return;
- sourceSaveError.value = e?.message || String(e);
+ sourceSaveError.value = formatObjectSourceSaveError(e, effectiveDatabaseType.value, row.type as ObjectSourceKind, t("objects.postgresViewColumnChangeHint"));
} finally {
if (sidePanelGuard.isFresh(epoch)) sourceSaving.value = false;
}
@@ -2890,7 +2890,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
force-word-wrap
@save="saveSource"
/>
-
diff --git a/apps/desktop/src/components/objects/ObjectSourceDialog.vue b/apps/desktop/src/components/objects/ObjectSourceDialog.vue
index f0bdb097d5..6cf5e4b526 100644
--- a/apps/desktop/src/components/objects/ObjectSourceDialog.vue
+++ b/apps/desktop/src/components/objects/ObjectSourceDialog.vue
@@ -7,7 +7,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { copyToClipboard } from "@/lib/common/clipboard";
import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
-import { buildEditableObjectSource, buildExecutableObjectSourceStatements, executeObjectSourceSave } from "@/lib/table/objectSourceEditor";
+import { buildEditableObjectSource, buildExecutableObjectSourceStatements, executeObjectSourceSave, formatObjectSourceSaveError } from "@/lib/table/objectSourceEditor";
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
import * as api from "@/lib/backend/api";
import QueryEditor from "@/components/editor/QueryEditor.vue";
@@ -169,8 +169,8 @@ async function saveSource() {
toast(t("objects.sourceSaved"));
emit("saved");
await loadSource(false);
- } catch (e: any) {
- saveError.value = e?.message || String(e);
+ } catch (e: unknown) {
+ saveError.value = formatObjectSourceSaveError(e, databaseType, props.objectType, t("objects.postgresViewColumnChangeHint"));
} finally {
saving.value = false;
}
@@ -213,7 +213,7 @@ function closeDialog() {
hide-execution-controls
@save="saveSource"
/>
-
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index d6006920fd..03781d4031 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -2325,6 +2325,8 @@ export default {
cancelEdit: "Cancel",
sourceSaved: "Source saved",
sourceSaveFailed: "Failed to save source: {message}",
+ postgresViewColumnChangeHint:
+ "PostgreSQL cannot use CREATE OR REPLACE VIEW to remove, reorder, rename, or change the type of existing view columns. Keep existing column count, order, names, and types unchanged; new columns can only be appended. Use ALTER VIEW ... RENAME COLUMN to rename a column. To remove a column, review dependencies and recreate the view manually.",
sourceReadOnly: "This source is read-only and cannot be edited.",
schemaColumn: "Schema",
comment: "Comment",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 348feaf886..4b7eb3e106 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -2175,6 +2175,8 @@ export default withEnglishFallback({
cancelEdit: "Cancelar",
sourceSaved: "Código fuente guardado",
sourceSaveFailed: "Error al guardar el código fuente: {message}",
+ postgresViewColumnChangeHint:
+ "PostgreSQL no permite usar CREATE OR REPLACE VIEW para eliminar, reordenar, renombrar o cambiar el tipo de las columnas existentes de una vista. Mantenga sin cambios la cantidad, el orden, los nombres y los tipos de las columnas existentes; las columnas nuevas solo se pueden agregar al final. Use ALTER VIEW ... RENAME COLUMN para renombrar una columna. Para eliminar una columna, revise las dependencias y vuelva a crear la vista manualmente.",
schemaColumn: "Esquema",
comment: "Comentario",
loadingSchemas: "Cargando esquemas...",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index ba6a46f776..7085f21734 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -2173,6 +2173,8 @@ export default withEnglishFallback({
cancelEdit: "Annulla",
sourceSaved: "Sorgente salvato",
sourceSaveFailed: "Impossibile salvare il sorgente: {message}",
+ postgresViewColumnChangeHint:
+ "PostgreSQL non consente di usare CREATE OR REPLACE VIEW per eliminare, riordinare, rinominare o cambiare il tipo delle colonne esistenti di una vista. Mantieni invariati il numero, l'ordine, i nomi e i tipi delle colonne esistenti; le nuove colonne possono essere aggiunte solo alla fine. Usa ALTER VIEW ... RENAME COLUMN per rinominare una colonna. Per eliminare una colonna, verifica le dipendenze e ricrea manualmente la vista.",
schemaColumn: "Schema",
comment: "Commento",
loadingSchemas: "Caricamento schemi...",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 8afccd7127..65da9be1ba 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -2208,6 +2208,8 @@ export default withEnglishFallback({
cancelEdit: "キャンセル",
sourceSaved: "ソースを保存しました",
sourceSaveFailed: "ソースの保存に失敗しました: {message}",
+ postgresViewColumnChangeHint:
+ "PostgreSQL では CREATE OR REPLACE VIEW を使用して、既存のビュー列の削除、並べ替え、名前変更、または型変更を行うことはできません。既存列の数、順序、名前、型を維持してください。新しい列は末尾にのみ追加できます。列名の変更には ALTER VIEW ... RENAME COLUMN を使用してください。列を削除する場合は、依存関係を確認してビューを手動で再作成してください。",
schemaColumn: "スキーマ",
comment: "コメント",
loadingSchemas: "スキーマを読み込み中...",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 7c4037e217..a8ffea37f2 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -2175,6 +2175,8 @@ export default withEnglishFallback({
cancelEdit: "Cancelar",
sourceSaved: "Código-fonte salvo",
sourceSaveFailed: "Falha ao salvar código-fonte: {message}",
+ postgresViewColumnChangeHint:
+ "O PostgreSQL não permite usar CREATE OR REPLACE VIEW para remover, reordenar, renomear ou alterar o tipo das colunas existentes de uma view. Mantenha inalterados a quantidade, a ordem, os nomes e os tipos das colunas existentes; novas colunas só podem ser adicionadas ao final. Use ALTER VIEW ... RENAME COLUMN para renomear uma coluna. Para remover uma coluna, revise as dependências e recrie a view manualmente.",
schemaColumn: "Schema",
comment: "Comentário",
loadingSchemas: "Carregando schemas...",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 19cc103480..09b5b6e14d 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -2314,6 +2314,7 @@ export default withEnglishFallback({
cancelEdit: "取消",
sourceSaved: "源码已保存",
sourceSaveFailed: "保存源码失败:{message}",
+ postgresViewColumnChangeHint: "PostgreSQL 无法通过 CREATE OR REPLACE VIEW 删除、重排、重命名已有视图字段,或修改字段类型。请保持原字段的数量、顺序、名称和类型不变;新增字段只能追加在末尾。重命名字段请使用 ALTER VIEW ... RENAME COLUMN;删除字段前请检查依赖并手动重建视图。",
sourceReadOnly: "该源码为只读,不能编辑。",
schemaColumn: "Schema",
comment: "注释",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index af2c9f4e5a..df60f16965 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -2044,6 +2044,7 @@ export default withEnglishFallback({
cancelEdit: "取消",
sourceSaved: "原始碼已儲存",
sourceSaveFailed: "儲存原始碼失敗:{message}",
+ postgresViewColumnChangeHint: "PostgreSQL 無法透過 CREATE OR REPLACE VIEW 刪除、重新排序、重新命名既有檢視欄位,或修改欄位型別。請保持原欄位的數量、順序、名稱和型別不變;新增欄位只能附加在末尾。重新命名欄位請使用 ALTER VIEW ... RENAME COLUMN;刪除欄位前請檢查相依性並手動重建檢視。",
sourceReadOnly: "此原始碼為唯讀,不能編輯。",
schemaColumn: "Schema",
comment: "註解",
diff --git a/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts b/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts
index 78e692d394..31695a6d8d 100644
--- a/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts
+++ b/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "@/lib/backend/api";
-import { executeObjectSourceSave } from "@/lib/table/objectSourceEditor";
+import { executeObjectSourceSave, formatObjectSourceSaveError } from "@/lib/table/objectSourceEditor";
vi.mock("@/lib/backend/api", () => ({
executeInTransaction: vi.fn().mockResolvedValue({}),
@@ -32,3 +32,33 @@ describe("executeObjectSourceSave", () => {
expect(api.executeScript).not.toHaveBeenCalled();
});
});
+
+describe("formatObjectSourceSaveError", () => {
+ const hint = "Keep existing view columns unchanged and append new columns at the end.";
+
+ it.each(["ERROR: cannot drop columns from view", 'ERROR: cannot change name of view column "old_name" to "new_name"', 'ERROR: cannot change data type of view column "amount" from integer to bigint', "ERROR: ビューからは列を削除できません", "错误:无法从视图中删除列"])(
+ "appends guidance for PostgreSQL view column replacement errors: %s",
+ (message) => {
+ expect(formatObjectSourceSaveError(new Error(message), "postgres", "VIEW", hint)).toBe(`${message}\n\n${hint}`);
+ },
+ );
+
+ it("supports PostgreSQL-compatible databases", () => {
+ expect(formatObjectSourceSaveError("cannot change name of view column", "kingbase", "VIEW", hint)).toBe(`cannot change name of view column\n\n${hint}`);
+ });
+
+ it("leaves unrelated errors unchanged", () => {
+ expect(formatObjectSourceSaveError(new Error("syntax error at or near SELECT"), "postgres", "VIEW", hint)).toBe("syntax error at or near SELECT");
+ expect(formatObjectSourceSaveError(new Error("SQLSTATE 42P16"), "mysql", "VIEW", hint)).toBe("SQLSTATE 42P16");
+ expect(formatObjectSourceSaveError(new Error("SQLSTATE 42P16"), "postgres", "PROCEDURE", hint)).toBe("SQLSTATE 42P16");
+ });
+
+ it.each(["Agent RPC error: SQLSTATE 42P16", "ERROR: ON COMMIT can only be used on temporary tables (SQLSTATE 42P16)"])("does not treat unrelated invalid_table_definition errors as view column changes: %s", (message) => {
+ expect(formatObjectSourceSaveError(new Error(message), "postgres", "VIEW", hint)).toBe(message);
+ });
+
+ it("does not append the same guidance twice", () => {
+ const formatted = formatObjectSourceSaveError("ERROR: cannot drop columns from view", "postgres", "VIEW", hint);
+ expect(formatObjectSourceSaveError(formatted, "postgres", "VIEW", hint)).toBe(formatted);
+ });
+});
diff --git a/apps/desktop/src/lib/table/objectSourceEditor.ts b/apps/desktop/src/lib/table/objectSourceEditor.ts
index 23397ca376..84fa30a4e6 100644
--- a/apps/desktop/src/lib/table/objectSourceEditor.ts
+++ b/apps/desktop/src/lib/table/objectSourceEditor.ts
@@ -16,9 +16,32 @@ export type BuildRoutineRenameObjectSourceInput = BuildEditableObjectSourceSqlIn
export type ObjectSourceSaveExecutionMode = "single" | "script";
const postgresLikeRoutineRenameTypes = new Set(["postgres", "redshift", "gaussdb", "kwdb", "kingbase", "highgo", "vastbase"]);
+const postgresLikeViewTypes = new Set(["postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "kingbase", "highgo", "vastbase"]);
const mysqlLikeRoutineRenameTypes = new Set(["mysql", "goldendb"]);
const oracleLikeRoutineRenameTypes = new Set(["oracle", "dameng"]);
+// SQLSTATE 42P16 covers unrelated invalid table definitions, so only match the
+// confirmed PostgreSQL view-column errors and their localized equivalents.
+const postgresViewColumnChangeErrorPatterns = [/cannot drop columns? from view/i, /cannot change name of view column/i, /cannot change data type of view column/i, /ビューからは列を削除できません/, /(?:无法|不能)从视图中删除列/];
+
+function errorMessage(error: unknown): string {
+ if (error instanceof Error) return error.message;
+ if (error && typeof error === "object" && "message" in error) {
+ const message = (error as { message?: unknown }).message;
+ if (message !== undefined && message !== null) return String(message);
+ }
+ return String(error);
+}
+
+export function formatObjectSourceSaveError(error: unknown, databaseType: DatabaseType, objectType: ObjectSourceKind, postgresViewColumnChangeHint: string): string {
+ const message = errorMessage(error);
+ if (objectType !== "VIEW" || !postgresLikeViewTypes.has(databaseType) || !postgresViewColumnChangeErrorPatterns.some((pattern) => pattern.test(message)) || message.includes(postgresViewColumnChangeHint)) {
+ return message;
+ }
+
+ return `${message}\n\n${postgresViewColumnChangeHint}`;
+}
+
export function supportsSourceBackedRoutineRename(databaseType: DatabaseType | undefined, objectType: ObjectSourceKind): boolean {
if (objectType !== "FUNCTION" && objectType !== "PROCEDURE") return false;
if (!databaseType || databaseType === "sqlserver") return false;