mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 12:41:39 +00:00
fix(postgres): improve view column save guidance
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
<div v-if="sourceSaveError" class="shrink-0 border-t px-3 py-2 text-xs text-destructive">
|
||||
<div v-if="sourceSaveError" class="shrink-0 whitespace-pre-wrap break-words border-t px-3 py-2 text-xs text-destructive">
|
||||
{{ sourceSaveError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<div v-if="saveError" class="shrink-0 border-t px-3 py-2 text-xs text-destructive">
|
||||
<div v-if="saveError" class="shrink-0 whitespace-pre-wrap break-words border-t px-3 py-2 text-xs text-destructive">
|
||||
{{ saveError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -2208,6 +2208,8 @@ export default withEnglishFallback({
|
||||
cancelEdit: "キャンセル",
|
||||
sourceSaved: "ソースを保存しました",
|
||||
sourceSaveFailed: "ソースの保存に失敗しました: {message}",
|
||||
postgresViewColumnChangeHint:
|
||||
"PostgreSQL では CREATE OR REPLACE VIEW を使用して、既存のビュー列の削除、並べ替え、名前変更、または型変更を行うことはできません。既存列の数、順序、名前、型を維持してください。新しい列は末尾にのみ追加できます。列名の変更には ALTER VIEW ... RENAME COLUMN を使用してください。列を削除する場合は、依存関係を確認してビューを手動で再作成してください。",
|
||||
schemaColumn: "スキーマ",
|
||||
comment: "コメント",
|
||||
loadingSchemas: "スキーマを読み込み中...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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: "注释",
|
||||
|
||||
@@ -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: "註解",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,9 +16,32 @@ export type BuildRoutineRenameObjectSourceInput = BuildEditableObjectSourceSqlIn
|
||||
export type ObjectSourceSaveExecutionMode = "single" | "script";
|
||||
|
||||
const postgresLikeRoutineRenameTypes = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "kingbase", "highgo", "vastbase"]);
|
||||
const postgresLikeViewTypes = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "kingbase", "highgo", "vastbase"]);
|
||||
const mysqlLikeRoutineRenameTypes = new Set<DatabaseType>(["mysql", "goldendb"]);
|
||||
const oracleLikeRoutineRenameTypes = new Set<DatabaseType>(["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;
|
||||
|
||||
Reference in New Issue
Block a user