Revert "fix(objects): guard system source edits"

This reverts commit 17bc2e66d4.
This commit is contained in:
t8y2 2026-05-18 17:53:02 +08:00
parent 2172b624b1
commit af16ac219c
13 changed files with 27 additions and 291 deletions

View File

@ -218,26 +218,6 @@ function openAiPanel() {
}
}
function isActiveObjectSourceReadOnly() {
return !!activeTab.value?.objectSource?.readOnlyReason;
}
function guardWritableObjectSource() {
if (!isActiveObjectSourceReadOnly()) return true;
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return false;
}
function tryExecuteActiveTab() {
if (!guardWritableObjectSource()) return;
tryExecute();
}
function tryExplainActiveTab() {
if (!guardWritableObjectSource()) return;
tryExplain();
}
function analyzeHistoryWithAi(entry: HistoryEntry) {
const connectionId = entry.connection_id || activeTab.value?.connectionId;
if (!connectionId) {
@ -274,7 +254,6 @@ async function openSaveSqlDialog() {
const tab = activeTab.value;
if (!tab || !tab.sql.trim()) return;
if (tab.objectSource) {
if (!guardWritableObjectSource()) return;
await saveActiveObjectSource(tab);
return;
}
@ -304,10 +283,6 @@ async function saveActiveObjectSource(tab: NonNullable<typeof activeTab.value>)
const connection = connectionStore.getConfig(tab.connectionId);
const source = tab.objectSource;
if (!connection || !source) return;
if (source.readOnlyReason) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return;
}
try {
const statements = buildExecutableObjectSourceStatements({
@ -595,7 +570,7 @@ function handleKeydown(e: KeyboardEvent) {
) {
e.preventDefault();
e.stopPropagation();
tryExecuteActiveTab();
tryExecute();
}
}
@ -789,9 +764,9 @@ onUnmounted(() => {
:active-tab="activeTab"
:active-connection="activeConnection"
:executable-sql="executableSql"
@execute="tryExecuteActiveTab"
@execute="tryExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplainActiveTab"
@explain="tryExplain()"
@format-sql="formatActiveSql"
@save-sql="void openSaveSqlDialog()"
@open-sql="openSqlFile"
@ -812,9 +787,9 @@ onUnmounted(() => {
:cursor-pos="cursorPos"
@update:active-output-view="activeOutputView = $event"
@fix-with-ai="fixWithAi"
@execute="tryExecuteActiveTab"
@execute="tryExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplainActiveTab"
@explain="tryExplain()"
@editor-update="
(v: string) => {
if (queryStore.activeTabId) queryStore.updateSql(queryStore.activeTabId, v);

View File

@ -28,7 +28,6 @@ const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explai
const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue"));
import { useQueryStore } from "@/stores/queryStore";
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState";
import { shouldShowQueryOutputPane } from "@/lib/contentAreaLayout";
import { databaseDisplayNameForTab } from "@/lib/tabPresentation";
import { isTableDataEditable } from "@/lib/tableEditing";
import type { QueryTab, ConnectionConfig } from "@/types/database";
@ -115,9 +114,6 @@ const activeQueryError = computed(() => {
return String(result.rows[0]?.[0] ?? "");
});
const showQueryOutputPane = computed(() => shouldShowQueryOutputPane(props.activeTab));
const objectSourceReadOnly = computed(() => !!props.activeTab.objectSource?.readOnlyReason);
// Column info panel handlers
async function onHandleClickColumn(
matchedCols: Array<{ name: string; table: string; schema?: string }>,
@ -212,7 +208,7 @@ defineExpose({ focusSearch, refreshData });
<!-- Query mode: editor + results -->
<template v-if="activeTab.mode === 'query'">
<Splitpanes horizontal class="flex-1">
<Pane :size="showQueryOutputPane ? 40 : 100" :min-size="showQueryOutputPane ? 15 : 100">
<Pane :size="40" :min-size="15">
<div class="h-full flex flex-col relative">
<QueryEditor
class="flex-1"
@ -223,7 +219,6 @@ defineExpose({ focusSearch, refreshData });
:format-dialect="activeSqlFormatDialect"
:format-request-id="formatSqlRequestId"
:execution-error="activeQueryError"
:read-only="objectSourceReadOnly"
@update:model-value="emit('editorUpdate', $event)"
@selection-change="emit('editorSelectionChange', $event)"
@cursor-change="emit('editorCursorChange', $event)"
@ -243,7 +238,7 @@ defineExpose({ focusSearch, refreshData });
/>
</div>
</Pane>
<Pane v-if="showQueryOutputPane" :size="60" :min-size="20">
<Pane :size="60" :min-size="20">
<div class="h-full flex flex-col">
<div
v-if="

View File

@ -65,11 +65,7 @@ const activeConnectionValue = computed(() => props.activeConnection?.id || "");
const activeSchemaValue = computed(() => props.activeTab.schema || "");
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
const schemaDatabaseKey = computed(() => props.activeTab.database || (isSingleDb.value ? "_" : ""));
const objectSourceReadOnly = computed(() => !!props.activeTab.objectSource?.readOnlyReason);
const saveTooltip = computed(() => {
if (objectSourceReadOnly.value) return t("objects.sourceReadOnlySystemObject");
return props.activeTab.objectSource ? t("objects.saveSource") : t("toolbar.saveSql");
});
const saveTooltip = computed(() => (props.activeTab.objectSource ? t("objects.saveSource") : t("toolbar.saveSql")));
const showSchemaSelector = computed(() => {
const connection = props.activeConnection;
@ -128,10 +124,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
: 'bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200'
"
:disabled="
objectSourceReadOnly ||
activeTab.isCancelling ||
activeTab.isExplaining ||
(!activeTab.isExecuting && !executableSql.trim())
activeTab.isCancelling || activeTab.isExplaining || (!activeTab.isExecuting && !executableSql.trim())
"
@click="activeTab.isExecuting ? emit('cancel') : emit('execute')"
>
@ -155,9 +148,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
? ''
: 'text-violet-600 hover:bg-violet-500/10 hover:text-violet-700 dark:text-violet-300 dark:hover:text-violet-200'
"
:disabled="
objectSourceReadOnly || activeTab.isExecuting || (!activeTab.isExplaining && !executableSql.trim())
"
:disabled="activeTab.isExecuting || (!activeTab.isExplaining && !executableSql.trim())"
@click="activeTab.isExplaining ? emit('cancel') : emit('explain')"
>
<Square v-if="activeTab.isExplaining" class="h-3.5 w-3.5 fill-current" />
@ -174,7 +165,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
variant="ghost"
size="icon"
class="h-6 w-6 text-amber-600 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200"
:disabled="objectSourceReadOnly || activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()"
:disabled="activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()"
@click="emit('formatSql')"
>
<AlignLeft class="h-3.5 w-3.5" />
@ -188,7 +179,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
variant="ghost"
size="icon"
class="h-6 w-6 text-blue-600 hover:bg-blue-500/10 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200"
:disabled="objectSourceReadOnly || !activeTab.sql.trim()"
:disabled="!activeTab.sql.trim()"
@click="emit('saveSql')"
>
<Save class="h-3.5 w-3.5" />

View File

@ -34,11 +34,7 @@ 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,
objectSourceReadOnlyReason,
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";
@ -112,7 +108,6 @@ const sourceFormatDialect = computed<SqlFormatDialect>(() => {
return "generic";
}
});
const sourceRowReadOnlyReason = computed(() => (sourceRow.value ? readOnlyReasonForRow(sourceRow.value) : null));
const objectFilters = computed<ObjectFilter[]>(() =>
(
[
@ -174,17 +169,7 @@ function canOpenSource(row: ObjectBrowserRow) {
}
function canRename(row: ObjectBrowserRow) {
return !readOnlyReasonForRow(row) && supportsObjectRename(props.connection.db_type, row.type);
}
function readOnlyReasonForRow(row: ObjectBrowserRow) {
if (row.type !== "VIEW" && row.type !== "PROCEDURE" && row.type !== "FUNCTION") return null;
return objectSourceReadOnlyReason({
databaseType: props.connection.db_type,
schema: row.schema || selectedSchema.value || props.database,
name: row.name,
objectType: row.type,
});
return supportsObjectRename(props.connection.db_type, row.type);
}
function sourceTitle(row: ObjectBrowserRow | null) {
@ -220,7 +205,7 @@ async function openSource(row: ObjectBrowserRow) {
);
sourceContent.value = result.source;
sourceDraft.value = result.source;
sourceEditing.value = !readOnlyReasonForRow(row);
sourceEditing.value = true;
} catch (e: any) {
sourceError.value = e?.message || String(e);
} finally {
@ -250,19 +235,11 @@ function qualifiedName(row: ObjectBrowserRow): string {
}
function requestDrop(row: ObjectBrowserRow) {
if (readOnlyReasonForRow(row)) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return;
}
dropTarget.value = row;
showDropConfirm.value = true;
}
function requestRename(row: ObjectBrowserRow) {
if (readOnlyReasonForRow(row)) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return;
}
renameTarget.value = row;
renameInput.value = row.name;
renameError.value = "";
@ -290,10 +267,6 @@ async function confirmRename() {
const row = renameTarget.value;
const newName = renameInput.value.trim();
if (!row || !newName || newName === row.name) return;
if (readOnlyReasonForRow(row)) {
renameError.value = t("objects.sourceReadOnlySystemObject");
return;
}
renameError.value = "";
try {
const schema = row.schema || selectedSchema.value || props.database;
@ -322,11 +295,6 @@ async function confirmRename() {
async function confirmDrop() {
if (!dropTarget.value) return;
const row = dropTarget.value;
if (readOnlyReasonForRow(row)) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
dropTarget.value = null;
return;
}
const typeSql =
row.type === "VIEW"
? "VIEW"
@ -395,10 +363,6 @@ function copySource() {
function editSource() {
if (!sourceRow.value || !sourceContent.value) return;
if (sourceRowReadOnlyReason.value) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return;
}
sourceDraft.value = sourceContent.value;
sourceSaveError.value = "";
sourceEditing.value = true;
@ -412,10 +376,6 @@ function cancelEditSource() {
async function saveSource() {
if (!sourceRow.value || !sourceDraft.value.trim()) return;
if (sourceRowReadOnlyReason.value) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return;
}
const row = sourceRow.value;
const schema = row.schema || selectedSchema.value || props.database;
sourceSaving.value = true;
@ -691,13 +651,11 @@ watch(
<ContextMenuItem v-if="canRename(item)" @click="requestRename(item)">
<Pencil class="w-4 h-4 mr-2" /> {{ t("contextMenu.renameObject") }}
</ContextMenuItem>
<template v-if="!readOnlyReasonForRow(item)">
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDrop(item)">
<Trash2 class="w-4 h-4 mr-2" />
{{ item.type === "PROCEDURE" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }}
</ContextMenuItem>
</template>
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDrop(item)">
<Trash2 class="w-4 h-4 mr-2" />
{{ item.type === "PROCEDURE" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }}
</ContextMenuItem>
</template>
</ContextMenuContent>
</ContextMenu>
@ -743,8 +701,7 @@ watch(
variant="ghost"
size="icon"
class="h-5 w-5"
:disabled="!sourceContent || !!sourceRowReadOnlyReason"
:title="sourceRowReadOnlyReason ? t('objects.sourceReadOnlySystemObject') : undefined"
:disabled="!sourceContent"
@click="editSource"
>
<PencilLine class="h-3 w-3" />
@ -767,7 +724,6 @@ watch(
:database="props.database"
:dialect="sourceDialect"
:format-dialect="sourceFormatDialect"
:read-only="!!sourceRowReadOnlyReason"
force-word-wrap
@save="saveSource"
/>

View File

@ -92,7 +92,6 @@ import {
treeNodeRowAction,
treeNodeRowDoubleClickAction,
} from "@/lib/treeNodeClick";
import { objectSourceReadOnlyReason } from "@/lib/objectSourceEditor";
import { formatCsv, formatJson, formatSqlInsert } from "@/lib/exportFormats";
import { fetchTableDataForExport } from "@/lib/tableDataExport";
import {
@ -666,18 +665,6 @@ function viewObjectSource() {
schema,
name: node.label,
objectType,
readOnlyReason: (() => {
const databaseType = currentDatabaseType();
if (!databaseType) return undefined;
return (
objectSourceReadOnlyReason({
databaseType,
schema,
name: node.label,
objectType,
}) ?? undefined
);
})(),
});
})
.catch((e: any) => {
@ -686,10 +673,6 @@ function viewObjectSource() {
}
function requestDropObject() {
if (currentObjectSourceReadOnlyReason.value) {
toast(t("objects.sourceReadOnlySystemObject"), 5000);
return;
}
showDropObjectConfirm.value = true;
}
@ -703,21 +686,7 @@ function nodeRenameObjectType(): RenameableObjectType | null {
const canRenameObject = computed(() => {
const objectType = nodeRenameObjectType();
return (
!!objectType && !currentObjectSourceReadOnlyReason.value && supportsObjectRename(currentDatabaseType(), objectType)
);
});
const currentObjectSourceReadOnlyReason = computed(() => {
const objectType = objectSourceKindForTreeNode(props.node.type);
const databaseType = currentDatabaseType();
if (!objectType || !databaseType) return null;
return objectSourceReadOnlyReason({
databaseType,
schema: props.node.schema,
name: props.node.label,
objectType,
});
return !!objectType && supportsObjectRename(currentDatabaseType(), objectType);
});
function openRenameObjectDialog() {
@ -2005,13 +1974,11 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
<Pencil class="w-4 h-4 mr-2" />
{{ t("contextMenu.renameObject") }}
</ContextMenuItem>
<template v-if="!currentObjectSourceReadOnlyReason">
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDropObject">
<Trash2 class="w-4 h-4 mr-2" />
{{ node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }}
</ContextMenuItem>
</template>
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDropObject">
<Trash2 class="w-4 h-4 mr-2" />
{{ node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }}
</ContextMenuItem>
</template>
<template v-if="isGroupLabel(node)">

View File

@ -762,7 +762,6 @@ export default {
cancelEdit: "Cancel",
sourceSaved: "Source saved",
sourceSaveFailed: "Failed to save source: {message}",
sourceReadOnlySystemObject: "System objects are protected and cannot be modified or deleted",
schemaColumn: "Schema",
comment: "Comment",
loadingSchemas: "Loading schemas...",

View File

@ -667,7 +667,6 @@ export default {
cancelEdit: "Cancelar",
sourceSaved: "Código fuente guardado",
sourceSaveFailed: "Error al guardar el código fuente: {message}",
sourceReadOnlySystemObject: "Los objetos del sistema están protegidos y no se pueden modificar ni eliminar",
schemaColumn: "Esquema",
comment: "Comentario",
loadingSchemas: "Cargando esquemas...",

View File

@ -742,7 +742,6 @@ export default {
cancelEdit: "取消",
sourceSaved: "源码已保存",
sourceSaveFailed: "保存源码失败:{message}",
sourceReadOnlySystemObject: "系统对象受保护,不能修改或删除",
schemaColumn: "Schema",
comment: "注释",
loadingSchemas: "加载 Schema...",

View File

@ -1,13 +0,0 @@
import type { QueryTab } from "@/types/database";
type QueryOutputPaneState = Pick<
QueryTab,
"mode" | "objectSource" | "result" | "explainPlan" | "explainError" | "isExecuting" | "isExplaining"
>;
export function shouldShowQueryOutputPane(tab: QueryOutputPaneState): boolean {
if (tab.mode !== "query") return false;
if (!tab.objectSource) return true;
return !!(tab.result || tab.explainPlan || tab.explainError || tab.isExecuting || tab.isExplaining);
}

View File

@ -9,14 +9,6 @@ type BuildEditableObjectSourceSqlInput = {
};
export type ObjectSourceSaveExecutionMode = "single" | "script";
export type ObjectSourceReadOnlyReason = "system-object";
type ObjectSourceReadOnlyInput = {
databaseType: DatabaseType;
schema?: string | null;
name: string;
objectType: ObjectSourceKind;
};
const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
"postgres",
@ -106,37 +98,3 @@ export function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceS
export function objectSourceSaveExecutionMode(_databaseType: DatabaseType): ObjectSourceSaveExecutionMode {
return "single";
}
const readOnlySystemSchemas = new Set([
"information_schema",
"pg_catalog",
"sys",
"system",
"mysql",
"performance_schema",
"xdb",
"outln",
"dbsnmp",
"ctisys",
"sysauditor",
"syssso",
]);
const damengSysdbaRoutinePrefixes = ["SP_TS_", "SP_ARCH_", "SP_DB_", "SP_DROP_CONS_", "SP_TAB_", "SP_UPDATE_SYS"];
export function objectSourceReadOnlyReason(input: ObjectSourceReadOnlyInput): ObjectSourceReadOnlyReason | null {
const schema = input.schema?.trim().toLowerCase();
if (schema && readOnlySystemSchemas.has(schema)) return "system-object";
const name = input.name.trim().toUpperCase();
if (
input.databaseType === "dameng" &&
input.schema?.trim().toUpperCase() === "SYSDBA" &&
(input.objectType === "PROCEDURE" || input.objectType === "FUNCTION") &&
damengSysdbaRoutinePrefixes.some((prefix) => name.startsWith(prefix))
) {
return "system-object";
}
return null;
}

View File

@ -290,7 +290,6 @@ export interface QueryTab {
schema?: string;
name: string;
objectType: ObjectSourceKind;
readOnlyReason?: "system-object";
};
tableMeta?: {
schema?: string;

View File

@ -1,54 +0,0 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import { shouldShowQueryOutputPane } from "../../apps/desktop/src/lib/contentAreaLayout.ts";
test("object source tabs hide the query output pane while idle", () => {
assert.equal(
shouldShowQueryOutputPane({
mode: "query",
objectSource: { objectType: "PROCEDURE", name: "refresh_cache" },
isExecuting: false,
isExplaining: false,
}),
false,
);
});
test("object source tabs show the query output pane after execution starts or returns feedback", () => {
assert.equal(
shouldShowQueryOutputPane({
mode: "query",
objectSource: { objectType: "FUNCTION", name: "calc_total" },
isExecuting: true,
isExplaining: false,
}),
true,
);
assert.equal(
shouldShowQueryOutputPane({
mode: "query",
objectSource: { objectType: "PROCEDURE", name: "refresh_cache" },
isExecuting: false,
isExplaining: false,
result: {
columns: ["Error"],
rows: [["syntax error"]],
affected_rows: 0,
execution_time_ms: 2,
},
}),
true,
);
});
test("regular query tabs keep the output pane visible before execution", () => {
assert.equal(
shouldShowQueryOutputPane({
mode: "query",
isExecuting: false,
isExplaining: false,
}),
true,
);
});

View File

@ -3,7 +3,6 @@ import test from "node:test";
import {
buildExecutableObjectSourceSql,
buildExecutableObjectSourceStatements,
objectSourceReadOnlyReason,
objectSourceSaveExecutionMode,
} from "../../apps/desktop/src/lib/objectSourceEditor.ts";
@ -105,37 +104,3 @@ test("object source SQL joins generated save statements for previews", () => {
'CREATE OR REPLACE PROCEDURE "public"."refresh_cache_v2"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;\nDROP PROCEDURE IF EXISTS "public"."refresh_cache"(mode text);',
);
});
test("system schemas open object source as read-only", () => {
assert.equal(
objectSourceReadOnlyReason({
databaseType: "oracle",
schema: "SYS",
name: "DBMS_STATS",
objectType: "PROCEDURE",
}),
"system-object",
);
});
test("Dameng built-in maintenance routines open as read-only without blocking user SP names", () => {
assert.equal(
objectSourceReadOnlyReason({
databaseType: "dameng",
schema: "SYSDBA",
name: "SP_TS_BAKSET_REMOVE_BATCH",
objectType: "PROCEDURE",
}),
"system-object",
);
assert.equal(
objectSourceReadOnlyReason({
databaseType: "dameng",
schema: "SYSDBA",
name: "SP_HELLO",
objectType: "PROCEDURE",
}),
null,
);
});