diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 1c93afa347..a04bb7aa03 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -762,7 +762,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); :initial-order-by-input="activeTab.orderByInput" :sql="activeTab.sql" :loading="activeTab.isExecuting" - :editable="isTableDataEditable(activeEffectiveDatabaseType, activeTableMeta?.primaryKeys ?? [])" + :editable="isTableDataEditable(activeEffectiveDatabaseType, activeTableMeta?.primaryKeys ?? [], activeTableMeta?.tableType)" context="table-data" :initial-where-input="activeTab.whereInput" :database-type="activeEffectiveDatabaseType" diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 2a63780cb0..e36ef3dfbd 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -686,6 +686,7 @@ async function openData() { queryStore.setTableMeta(tabId, { schema: tableSchema, tableName: node.label, + tableType: node.type === "view" ? "VIEW" : "TABLE", columns: [], primaryKeys: [], }); @@ -711,7 +712,7 @@ async function openData() { elapsed: elapsed(), }); columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label); - primaryKeys = editablePrimaryKeys(effectiveDbType, columns); + primaryKeys = editablePrimaryKeys(effectiveDbType, columns, node.type === "view" ? "VIEW" : "TABLE"); console.info("[DBX][openData:get-columns:done]", { traceId, columnCount: columns.length, @@ -721,6 +722,7 @@ async function openData() { queryStore.setTableMeta(tabId, { schema: tableSchema, tableName: node.label, + tableType: node.type === "view" ? "VIEW" : "TABLE", columns, primaryKeys, }); diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index e0230668e6..9ebe579e25 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -27,7 +27,7 @@ export function useDataGridActions(activeTab: ComputedRef) const config = connectionStore.getConfig(tab.connectionId); const effectiveDbType = effectiveDatabaseTypeForConnection(config); const tableMeta = tableMetaForDataTab(tab); - const primaryKeys = tab.tableMeta ? editablePrimaryKeys(effectiveDbType, tab.tableMeta.columns) : (tableMeta?.primaryKeys ?? []); + const primaryKeys = tab.tableMeta ? editablePrimaryKeys(effectiveDbType, tab.tableMeta.columns, tab.tableMeta.tableType) : (tableMeta?.primaryKeys ?? []); if (tab.tableMeta && primaryKeys.join("\0") !== tab.tableMeta.primaryKeys.join("\0")) { tab.tableMeta.primaryKeys = primaryKeys; } @@ -55,10 +55,11 @@ export function useDataGridActions(activeTab: ComputedRef) const config = connectionStore.getConfig(tab.connectionId); const querySchema = connectionObjectTreeQuerySchema(config, tab.database, tableMeta.schema); const columns = await api.getColumns(tab.connectionId, tab.database, querySchema, tableMeta.tableName); - const primaryKeys = editablePrimaryKeys(effectiveDatabaseTypeForConnection(config), columns); + const primaryKeys = editablePrimaryKeys(effectiveDatabaseTypeForConnection(config), columns, tableMeta.tableType); queryStore.setTableMeta(tab.id, { schema: tableMeta.schema, tableName: tableMeta.tableName, + tableType: tableMeta.tableType, columns, primaryKeys, }); diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index 4024da5a87..9e28bd4987 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -59,6 +59,7 @@ async function openTableTarget(target: NavigationTarget) { queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, + tableType: "TABLE", columns, primaryKeys, }); @@ -76,6 +77,7 @@ async function openTableTarget(target: NavigationTarget) { queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, + tableType: "TABLE", columns: [], primaryKeys: [], }); @@ -89,6 +91,7 @@ async function openTableTarget(target: NavigationTarget) { queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, + tableType: "TABLE", columns, primaryKeys, }); @@ -150,7 +153,7 @@ export function useNavigationTargets(dialogs: { showFieldLineageDialog: { value: queryStore.setTableMeta(tab.id, { ...tab.tableMeta!, columns, - primaryKeys: editablePrimaryKeys(effectiveDatabaseTypeForConnection(connection), columns), + primaryKeys: editablePrimaryKeys(effectiveDatabaseTypeForConnection(connection), columns, tab.tableMeta!.tableType), }); if (tab.id === queryStore.activeTabId) await reloadData(); } catch (e: any) { diff --git a/apps/desktop/src/lib/__tests__/tableEditing.spec.ts b/apps/desktop/src/lib/__tests__/tableEditing.spec.ts new file mode 100644 index 0000000000..6f48471a5e --- /dev/null +++ b/apps/desktop/src/lib/__tests__/tableEditing.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { DBX_ROWID_COLUMN, editablePrimaryKeys, isTableDataEditable } from "@/lib/tableEditing"; +import type { ColumnInfo } from "@/types/database"; + +function column(name: string, isPrimaryKey = false): ColumnInfo { + return { + name, + data_type: "varchar", + is_nullable: true, + column_default: null, + is_primary_key: isPrimaryKey, + extra: null, + }; +} + +describe("tableEditing", () => { + it("does not synthesize Oracle ROWID for views", () => { + expect(editablePrimaryKeys("oracle", [column("ID"), column("NAME")], "VIEW")).toEqual([]); + expect(editablePrimaryKeys("oracle", [column("ID"), column("NAME")], "TABLE")).toEqual([DBX_ROWID_COLUMN]); + }); + + it("treats view data tabs as readonly", () => { + expect(isTableDataEditable("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false); + }); +}); diff --git a/apps/desktop/src/lib/tableEditing.ts b/apps/desktop/src/lib/tableEditing.ts index c2c6b84d13..bb741d0adb 100644 --- a/apps/desktop/src/lib/tableEditing.ts +++ b/apps/desktop/src/lib/tableEditing.ts @@ -5,8 +5,13 @@ export const DBX_ROWID_COLUMN = "__DBX_ROWID"; export const DBX_NEO4J_ELEMENT_ID_COLUMN = "__DBX_ELEMENT_ID"; export const DBX_TDENGINE_TBNAME_COLUMN = "tbname"; -export function editablePrimaryKeys(databaseType: DatabaseType | undefined, columns: ColumnInfo[]): string[] { +function isViewTableType(tableType?: string): boolean { + return tableType?.toUpperCase().includes("VIEW") === true; +} + +export function editablePrimaryKeys(databaseType: DatabaseType | undefined, columns: ColumnInfo[], tableType?: string): string[] { const primaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name); + if (isViewTableType(tableType)) return primaryKeys; if (databaseType === "tdengine" && primaryKeys.length > 0) return [DBX_TDENGINE_TBNAME_COLUMN, ...primaryKeys]; const syntheticKey = getDatabaseCapability(databaseType).syntheticKey; if (syntheticKey === "oracle-rowid" && primaryKeys.length === 0) return [DBX_ROWID_COLUMN]; @@ -14,7 +19,8 @@ export function editablePrimaryKeys(databaseType: DatabaseType | undefined, colu return primaryKeys; } -export function isTableDataEditable(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean { +export function isTableDataEditable(databaseType: DatabaseType | undefined, primaryKeys: string[], tableType?: string): boolean { + if (isViewTableType(tableType)) return false; const cap = getDatabaseCapability(databaseType).tableData; if (cap.readonly) return false; if (cap.insert) return true; diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 7c799856b7..d6be134652 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1548,7 +1548,7 @@ export const useQueryStore = defineStore("query", () => { const pageLimit = TABLE_DATA_EXPORT_PAGE_SIZE; const effectiveDbType = effectiveDatabaseTypeForConnection(conn); - const primaryKeys = tab.tableMeta ? editablePrimaryKeys(effectiveDbType, tab.tableMeta.columns) : tableMeta.primaryKeys; + const primaryKeys = tab.tableMeta ? editablePrimaryKeys(effectiveDbType, tab.tableMeta.columns, tab.tableMeta.tableType) : tableMeta.primaryKeys; const fallbackOrderColumns = effectiveDbType === "sqlserver" && !primaryKeys.length ? tableMeta.columns.slice(0, 1).map((column) => column.name) : undefined; const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableIdentifier(effectiveDbType, tab.resultSortColumn)} ${tab.resultSortDirection.toUpperCase()}` : undefined; const orderBy = tab.orderByInput?.trim() || sortOrder; diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index e7b923144a..f71a164195 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -483,6 +483,7 @@ export interface QueryTab { tableMeta?: { schema?: string; tableName: string; + tableType?: string; columns: ColumnInfo[]; primaryKeys: string[]; };