diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 5f3e60272..3961293c2 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1341,6 +1341,7 @@ async function handleQuickOpenSelect(item: any) { database: item.database, schema: item.schema, tableName: item.objectName || item.tableName, + tableType: item.type === "view" ? "VIEW" : item.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE", }); } else if (item.type === "procedure" || item.type === "function" || item.type === "sequence" || item.type === "package" || item.type === "package-body") { // Open the object source in a source tab @@ -1857,6 +1858,7 @@ onUnmounted(() => { database: activeTab.database, schema: target.schema, tableName: target.tableName, + tableType: target.tableType, }) " @object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)" diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 23b174fed..ed6079c7b 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2461,7 +2461,7 @@ const persistedColumnOrderKeys = ref([]); const displayableColumnIndexes = computed(() => props.result.columns .map((column, index) => ({ column, index })) - .filter(({ column }) => !isHiddenGridColumn(props.databaseType, column, props.tableMeta?.primaryKeys ?? [])) + .filter(({ column }) => !isHiddenGridColumn(props.databaseType, column, props.tableMeta?.primaryKeys ?? [], props.tableMeta?.tableType)) .map(({ index }) => index), ); const goToColumnItems = computed(() => @@ -5004,7 +5004,7 @@ async function applyOrderBySearch() { orderBy: orderByClause, limit: pageSize.value, whereInput: currentWhereInput(), - includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys), + includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType), }); await props.onExecuteSql(sql); } catch (e: any) { @@ -5035,7 +5035,7 @@ async function applyWhereFilter() { orderBy: orderByInput.value.trim() || (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined), limit: pageSize.value, whereInput, - includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys), + includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType), }); await props.onExecuteSql(sql); } catch (e: any) { diff --git a/apps/desktop/src/components/layout/AppDialogs.vue b/apps/desktop/src/components/layout/AppDialogs.vue index 8ba200aa4..5d882148d 100644 --- a/apps/desktop/src/components/layout/AppDialogs.vue +++ b/apps/desktop/src/components/layout/AppDialogs.vue @@ -51,6 +51,7 @@ const emit = defineEmits<{ database: string; schema?: string; tableName: string; + tableType?: string; columnName?: string; }, ]; @@ -60,6 +61,7 @@ const emit = defineEmits<{ database: string; schema?: string; tableName: string; + tableType?: string; whereInput?: string; }, ]; diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index b5c927f9f..56c0455ca 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -136,7 +136,7 @@ const emit = defineEmits<{ viewTableData: [tableName: string]; viewTableDdl: [tableName: string]; editTableStructure: [tableName: string]; - openObjectTable: [target: { tableName: string; schema?: string }]; + openObjectTable: [target: { tableName: string; schema?: string; tableType?: string }]; objectSchemaChange: [schema: string | undefined]; structureEditorSaved: [commentChanged: boolean]; structureEditorClose: []; diff --git a/apps/desktop/src/components/lineage/FieldLineageDialog.vue b/apps/desktop/src/components/lineage/FieldLineageDialog.vue index 1478fe1c0..2adac3d4c 100644 --- a/apps/desktop/src/components/lineage/FieldLineageDialog.vue +++ b/apps/desktop/src/components/lineage/FieldLineageDialog.vue @@ -30,6 +30,7 @@ const emit = defineEmits<{ database: string; schema?: string; tableName: string; + tableType?: string; columnName?: string; }, ]; @@ -268,6 +269,7 @@ function openItemTarget(item: FieldLineageItem) { database: props.prefillDatabase, schema: item.schema || props.prefillSchema, tableName: item.table, + tableType: item.kind === "viewReference" ? "VIEW" : "TABLE", columnName: item.column || props.prefillColumn, }); } diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 71d9bd700..5d1e283dd 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -96,7 +96,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - openTable: [target: { tableName: string; schema?: string }]; + openTable: [target: { tableName: string; schema?: string; tableType?: string }]; schemaChange: [schema: string | undefined]; }>(); @@ -661,7 +661,7 @@ async function saveFileContent(content: string, defaultFileName: string, filterN } function openViewData(row: ObjectBrowserRow) { - emit("openTable", { tableName: row.name, schema: row.schema }); + emit("openTable", { tableName: row.name, schema: row.schema, tableType: row.type }); } function openStructureEditor(row: ObjectBrowserRow) { diff --git a/apps/desktop/src/components/search/DatabaseSearchDialog.vue b/apps/desktop/src/components/search/DatabaseSearchDialog.vue index 708b02cca..2ec22b3b1 100644 --- a/apps/desktop/src/components/search/DatabaseSearchDialog.vue +++ b/apps/desktop/src/components/search/DatabaseSearchDialog.vue @@ -29,6 +29,7 @@ const emit = defineEmits<{ database: string; schema?: string; tableName: string; + tableType?: string; whereInput?: string; }, ]; @@ -45,6 +46,7 @@ type SearchResultItem = { id: string; schema?: string; tableName: string; + tableType?: string; matchedColumns: string[]; preview: string; whereInput: string; @@ -223,6 +225,7 @@ async function searchTable(task: SearchTableTask, databaseType: DatabaseType, cu id: `${tableLabel}:${rowIndex}:${results.value.length}`, schema: task.schema, tableName: task.table.name, + tableType: task.table.table_type, matchedColumns, preview: rowPreview(result.columns, row, matchedColumns), whereInput, @@ -257,6 +260,7 @@ function openResult(item: SearchResultItem) { database: props.prefillDatabase, schema: item.schema, tableName: item.tableName, + tableType: item.tableType, whereInput: item.whereInput, }); } diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 43b70e099..0a3c41d71 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1283,7 +1283,7 @@ async function openData() { const columns = cachedTableMeta?.columns ?? []; const primaryKeys = cachedTableMeta?.primaryKeys ?? []; - const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys); + const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableType); const sql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: tableSchema, diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index 697d72850..874e40f3d 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -33,7 +33,7 @@ export function useDataGridActions(activeTab: ComputedRef) const effectiveDbType = effectiveDatabaseTypeForConnection(config); const tableMeta = tableMetaForDataTab(tab); const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : (tableMeta?.primaryKeys ?? []); - const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys); + const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableMeta?.tableType); return buildTableSelectSql({ databaseType: effectiveDbType, schema: tableMeta?.schema, diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index 52a1b0b92..6fa766ad8 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -14,6 +14,7 @@ export type NavigationTarget = { database: string; schema?: string; tableName: string; + tableType?: string; columnName?: string; whereInput?: string; }; @@ -55,13 +56,15 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab if (!config) throw new Error("Connection config not found"); const effectiveDbType = effectiveDatabaseTypeForConnection(config); const querySchema = metadataSchemaForConnection(config, target.database, target.schema); + const targetTableType = target.tableType ?? "TABLE"; if (config.db_type === "neo4j") { const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName); - const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns); + const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, undefined, targetTableType); const sql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: target.schema, tableName: target.tableName, + tableType: targetTableType, columns: columns.map((column) => column.name), primaryKeys, whereInput: target.whereInput, @@ -71,7 +74,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, - tableType: "TABLE", + tableType: targetTableType, columns, primaryKeys, }); @@ -82,6 +85,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab databaseType: effectiveDbType, schema: target.schema, tableName: target.tableName, + tableType: targetTableType, whereInput: target.whereInput, limit: pageLimit, }); @@ -89,7 +93,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, - tableType: "TABLE", + tableType: targetTableType, columns: [], primaryKeys: [], }); @@ -106,6 +110,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab databaseType: effectiveDbType, schema: target.schema, tableName: target.tableName, + tableType: targetTableType, whereInput: target.whereInput, limit: 0, }); @@ -115,12 +120,12 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab try { const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName); const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName).catch(() => []); - const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, indexes); - const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys); + const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, indexes, targetTableType); + const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, targetTableType); queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, - tableType: "TABLE", + tableType: targetTableType, columns, primaryKeys, }); @@ -129,6 +134,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab databaseType: effectiveDbType, schema: target.schema, tableName: target.tableName, + tableType: targetTableType, whereInput: target.whereInput, primaryKeys, columns: columns.map((column) => column.name), diff --git a/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts b/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts index 696f193eb..f6842ab79 100644 --- a/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { DBX_ROWID_COLUMN, canEditExistingTableRows, canUseKeylessRowPredicate, editablePrimaryKeys, editableRowIdentifierColumns, isClickHouseExistingRowReadonlyColumn, isTableDataEditable, supportsDataGridTransaction } from "@/lib/table/tableEditing"; +import { DBX_ROWID_COLUMN, canEditExistingTableRows, canUseKeylessRowPredicate, editablePrimaryKeys, editableRowIdentifierColumns, isClickHouseExistingRowReadonlyColumn, isTableDataEditable, supportsDataGridTransaction, usesSyntheticRowIdKey } from "@/lib/table/tableEditing"; import type { ColumnInfo, IndexInfo } from "@/types/database"; function column(name: string, isPrimaryKey = false): ColumnInfo { @@ -33,6 +33,11 @@ describe("tableEditing", () => { expect(isTableDataEditable("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false); }); + it("does not include Oracle ROWID for view data tabs", () => { + expect(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false); + expect(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "MATERIALIZED_VIEW")).toBe(false); + }); + it("allows keyless row predicates only for databases that support them", () => { expect(canUseKeylessRowPredicate("postgres", [])).toBe(true); expect(canUseKeylessRowPredicate("mysql", [])).toBe(true); diff --git a/apps/desktop/src/lib/table/tableEditing.ts b/apps/desktop/src/lib/table/tableEditing.ts index 4fc488519..a8c22adce 100644 --- a/apps/desktop/src/lib/table/tableEditing.ts +++ b/apps/desktop/src/lib/table/tableEditing.ts @@ -72,13 +72,14 @@ export function hiveTablePropertiesIndicateTransactional(result: { rows: readonl }); } -export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean { +export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[], tableType?: string): boolean { + if (isViewTableType(tableType)) return false; return primaryKeys.length === 1 && ((databaseType === "oracle" && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN) || (databaseType === "neo4j" && primaryKeys[0] === DBX_NEO4J_ELEMENT_ID_COLUMN)); } -export function isHiddenGridColumn(databaseType: DatabaseType | undefined, column: string, primaryKeys: string[]): boolean { +export function isHiddenGridColumn(databaseType: DatabaseType | undefined, column: string, primaryKeys: string[], tableType?: string): boolean { if (databaseType === "neo4j" && column === DBX_NEO4J_ELEMENT_ID_COLUMN) return true; - return usesSyntheticRowIdKey(databaseType, primaryKeys) && column.toUpperCase() === DBX_ROWID_COLUMN; + return usesSyntheticRowIdKey(databaseType, primaryKeys, tableType) && column.toUpperCase() === DBX_ROWID_COLUMN; } export function isTdengineExistingRowReadonlyColumn(databaseType: DatabaseType | undefined, column: string, columns: ColumnInfo[]): boolean { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 7895f0589..da60cb9fd 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1687,10 +1687,12 @@ export const useQueryStore = defineStore("query", () => { elapsed: elapsed?.(), }); const indexes = await api.listIndexes(tab.connectionId, tab.database, metadataSchema, metadataTableName).catch(() => []); - const primaryKeys = editableRowIdentifierColumns(dbType as DatabaseType, columns, indexes); + const tableType = tab.tableMeta?.tableType; + const primaryKeys = editableRowIdentifierColumns(dbType as DatabaseType, columns, indexes, tableType); const tableMeta = { schema: metadataSchema || undefined, tableName: metadataTableName, + tableType, columns, primaryKeys, }; diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index a4f81c474..5353895a8 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -29,8 +29,13 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String }; let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty()).or(default_order_by.as_deref()); let order = order_by.map(|order_by| format!(" ORDER BY {order_by}")).unwrap_or_default(); + // Oracle join views can raise ORA-01445 when ROWID is selected; keep the + // synthetic ROWID fallback scoped to base-table reads. + let include_oracle_row_id = options.include_row_id + && database_type == Some(DatabaseType::Oracle) + && !is_view_table_type(options.table_type.as_deref()); - let select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { + let select_columns = if include_oracle_row_id { format!("ROWIDTOCHAR(t.ROWID) AS \"{DBX_ROWID_COLUMN}\", t.*") } else { build_select_columns( @@ -40,7 +45,7 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String ) }; let rownum_select_columns = quoted_table_columns_or_star(database_type, &options.columns); - let page_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { + let page_select_columns = if include_oracle_row_id { if options.columns.is_empty() { "*".to_string() } else { @@ -49,11 +54,8 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String } else { rownum_select_columns.clone() }; - let table_alias = if options.include_row_id && database_type.is_some_and(uses_fetch_first) { - format!("{table} t") - } else { - table - }; + let table_alias = + if include_oracle_row_id && database_type.is_some_and(uses_fetch_first) { format!("{table} t") } else { table }; match table_pagination_strategy(database_type) { TablePaginationStrategy::IrisTop => { @@ -88,11 +90,8 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String ) } TablePaginationStrategy::Rownum => { - let rownum_inner_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { - &select_columns - } else { - &rownum_select_columns - }; + let rownum_inner_select_columns = + if include_oracle_row_id { &select_columns } else { &rownum_select_columns }; build_rownum_table_select_sql( &table_alias, &where_clause, @@ -136,6 +135,10 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String } } +fn is_view_table_type(table_type: Option<&str>) -> bool { + table_type.is_some_and(|value| value.to_ascii_uppercase().contains("VIEW")) +} + pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String { let database_type = options.database_type; let table = qualified_table_name(database_type, options.schema, options.table_name); diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index e7ca1aae6..343259733 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -699,6 +699,23 @@ fn builds_oracle_and_neo4j_table_data_queries() { }), "SELECT \"__DBX_ROWID\", \"ID\", \"NAME\" FROM (SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t) WHERE ROWNUM <= 100" ); + assert_eq!( + build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Oracle), + schema: Some("DBXTEST".to_string()), + table_name: "DBX_JOIN_VIEW".to_string(), + table_type: Some("VIEW".to_string()), + primary_keys: vec![DBX_ROWID_COLUMN.to_string()], + columns: vec!["ID".to_string(), "NAME".to_string()], + fallback_order_columns: Vec::new(), + order_by: None, + limit: Some(100), + offset: None, + where_input: None, + include_row_id: true, + }), + "SELECT \"ID\", \"NAME\" FROM (SELECT \"ID\", \"NAME\" FROM \"DBXTEST\".\"DBX_JOIN_VIEW\") WHERE ROWNUM <= 100" + ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Neo4j), diff --git a/packages/app-tests/tableEditing.test.ts b/packages/app-tests/tableEditing.test.ts index ccf74c90f..eb34ceccf 100644 --- a/packages/app-tests/tableEditing.test.ts +++ b/packages/app-tests/tableEditing.test.ts @@ -134,6 +134,8 @@ test("keeps TDengine existing row identity and tag columns read-only", () => { test("detects the synthetic Oracle ROWID key case", () => { assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN]), true); assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN.toLowerCase()]), true); + assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "VIEW"), false); + assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "MATERIALIZED_VIEW"), false); assert.equal(usesSyntheticRowIdKey("postgres", [DBX_ROWID_COLUMN]), false); assert.equal(usesSyntheticRowIdKey("oracle", ["ID"]), false); assert.equal(usesSyntheticRowIdKey("neo4j", [DBX_NEO4J_ELEMENT_ID_COLUMN]), true); @@ -141,6 +143,7 @@ test("detects the synthetic Oracle ROWID key case", () => { test("hides only the synthetic Oracle ROWID grid column", () => { assert.equal(isHiddenGridColumn("oracle", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN]), true); + assert.equal(isHiddenGridColumn("oracle", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN], "VIEW"), false); assert.equal(isHiddenGridColumn("oracle", "ROWID", [DBX_ROWID_COLUMN]), false); assert.equal(isHiddenGridColumn("mysql", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN]), false); assert.equal(isHiddenGridColumn("neo4j", DBX_NEO4J_ELEMENT_ID_COLUMN, [DBX_NEO4J_ELEMENT_ID_COLUMN]), true);