From cab0e79dadb9d5e85a1124d5bffd4aef2e97bed7 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 11 May 2026 16:35:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(oracle):=20=E4=BF=AE=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD=E4=B8=8E=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/dbx-core/src/db/oracle_driver.rs | 32 ++++- crates/dbx-core/src/query.rs | 4 + src-tauri/src/commands/query.rs | 23 +++- src/components/grid/DataGrid.vue | 159 ++++++++++++++++-------- src/components/sidebar/TreeItem.vue | 68 +++++++--- src/composables/useDataGridActions.ts | 11 +- src/composables/useDataGridEditor.ts | 41 +++++- src/composables/useDataGridExport.ts | 4 +- src/composables/useNavigationTargets.ts | 19 ++- src/lib/dataGridSql.ts | 111 +++++++++++++++-- src/lib/tableEditing.ts | 21 ++++ src/lib/tableSelectSql.ts | 20 ++- src/stores/queryStore.ts | 36 ++++++ tests/dataGridSql.test.ts | 64 +++++++++- tests/queryStore.test.ts | 20 +++ tests/tableEditing.test.ts | 45 +++++++ tests/tableSelectSql.test.ts | 22 +++- 17 files changed, 610 insertions(+), 90 deletions(-) create mode 100644 src/lib/tableEditing.ts create mode 100644 tests/queryStore.test.ts create mode 100644 tests/tableEditing.test.ts diff --git a/crates/dbx-core/src/db/oracle_driver.rs b/crates/dbx-core/src/db/oracle_driver.rs index d4006f898..363843a5a 100644 --- a/crates/dbx-core/src/db/oracle_driver.rs +++ b/crates/dbx-core/src/db/oracle_driver.rs @@ -332,10 +332,12 @@ pub async fn list_triggers(conn: &OracleClient, schema: &str, table: &str) -> Re pub async fn execute_query_with_schema(conn: &OracleClient, schema: &str, sql: &str) -> Result { let set_schema = format!("ALTER SESSION SET CURRENT_SCHEMA = \"{}\"", schema); + log::info!("[oracle][set-schema:start] schema={schema}"); conn.execute(&set_schema, &[]).await.map_err(|e| { log::error!("[oracle] set current_schema failed: {e}"); e.to_string() })?; + log::info!("[oracle][set-schema:done] schema={schema}"); execute_query(conn, sql).await } @@ -343,17 +345,31 @@ pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result = result.columns.iter().map(|c| c.name.clone()).collect(); let mut rows: Vec> = result .rows @@ -369,11 +385,24 @@ pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result { let _ = conn.commit().await; + log::info!( + "[oracle][execute-non-select:done] affected_rows={} elapsed_ms={}", + result.rows_affected, + start.elapsed().as_millis() + ); Ok(QueryResult { columns: vec![], rows: vec![], @@ -384,8 +413,9 @@ pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result { let msg = e.to_string(); + log::error!("[oracle][execute-non-select:error] {msg}"); if msg.contains("Server rejected") || msg.contains("closed the connection") { - Err("Operation failed (connection closed) — possibly a constraint violation (foreign key, unique, or check constraint).".to_string()) + Err(format!("Operation failed (connection closed). Original driver error: {msg}")) } else { Err(msg) } diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index b500aab33..cc819cb27 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -205,6 +205,7 @@ pub async fn do_execute( let client = pool.client(); let schema = schema.map(|s| s.to_string()); drop(connections); + log::info!("[query][oracle:lock:start] schema={:?} sql={}", schema, sql); let client = match cancel_token.as_ref() { Some(token) => tokio::select! { biased; @@ -213,6 +214,7 @@ pub async fn do_execute( }, None => client.lock().await, }; + log::info!("[query][oracle:lock:done] schema={:?}", schema); if let Some(schema) = schema { wait_for_query(cancel_token, db::oracle_driver::execute_query_with_schema(&*client, &schema, sql)) .await @@ -635,9 +637,11 @@ async fn exec_tx_none_inner( ) -> Result { let mut total_affected: u64 = 0; for (i, sql) in statements.iter().enumerate() { + log::info!("[query][tx-none:statement:start] index={} sql={}", i + 1, sql); match do_execute(state, pool_key, sql, schema, None).await { Ok(result) => { total_affected += result.affected_rows; + log::info!("[query][tx-none:statement:done] index={} affected_rows={}", i + 1, result.affected_rows); } Err(e) => { log::warn!("Statement {} failed (no transaction support): {}", i + 1, e); diff --git a/src-tauri/src/commands/query.rs b/src-tauri/src/commands/query.rs index 387d6a587..b45ca02fd 100644 --- a/src-tauri/src/commands/query.rs +++ b/src-tauri/src/commands/query.rs @@ -37,8 +37,29 @@ pub async fn execute_multi( let registered_query = execution_id.as_ref().filter(|id| !id.trim().is_empty()).map(|id| state.running_queries.register(id.clone())); let cancel_token = registered_query.as_ref().map(|query| query.token()); + let trace_id = execution_id.as_deref().unwrap_or("no-execution-id"); + log::info!( + "[query][execute_multi:start] trace_id={} connection_id={} database={} schema={:?} sql={}", + trace_id, + connection_id, + database, + schema, + sql + ); - dbx_core::query::execute_multi_core(&state, &connection_id, &database, &sql, schema.as_deref(), cancel_token).await + let result = + dbx_core::query::execute_multi_core(&state, &connection_id, &database, &sql, schema.as_deref(), cancel_token) + .await; + match &result { + Ok(results) => log::info!( + "[query][execute_multi:done] trace_id={} result_count={} row_counts={:?}", + trace_id, + results.len(), + results.iter().map(|result| result.rows.len()).collect::>() + ), + Err(error) => log::error!("[query][execute_multi:error] trace_id={} error={}", trace_id, error), + } + result } #[tauri::command] diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index c36500d4c..0bd87cd10 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -60,6 +60,7 @@ import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import type { QueryResult, ColumnInfo, DatabaseType } from "@/types/database"; import * as api from "@/lib/api"; import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql"; +import { isHiddenGridColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing"; import { formatGridSqlLiteral } from "@/lib/dataGridSql"; import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus"; @@ -726,11 +727,25 @@ const isApplyingWhere = ref(false); const rowStatusFilter = ref("all"); const gridRef = ref(); const headerRef = ref(); +const visibleColumnIndexes = computed(() => + props.result.columns + .map((column, index) => ({ column, index })) + .filter(({ column }) => !isHiddenGridColumn(props.databaseType, column, props.tableMeta?.primaryKeys ?? [])) + .map(({ index }) => index), +); +const visibleColumns = computed(() => visibleColumnIndexes.value.map((index) => props.result.columns[index])); +const visibleRows = computed(() => + props.result.rows.map((row) => visibleColumnIndexes.value.map((index) => row[index])), +); +const firstVisibleColumnIndex = computed(() => visibleColumnIndexes.value[0] ?? 0); +function actualColumnIndex(visibleColumnIndex: number): number { + return visibleColumnIndexes.value[visibleColumnIndex] ?? visibleColumnIndex; +} // --- Column resize composable --- const { initColumnWidths, onResizeStart, autoFitColumn, columnVars, getIsResizing } = useDataGridColumnResize({ - columns: computed(() => props.result.columns), - rows: computed(() => props.result.rows), + columns: visibleColumns, + rows: visibleRows, gridRef, }); function syncHeaderScroll(e: Event) { @@ -740,7 +755,7 @@ function syncHeaderScroll(e: Event) { } initColumnWidths(); -watch(() => props.result.columns.length, initColumnWidths); +watch(() => visibleColumns.value.length, initColumnWidths); watch( () => props.result, () => { @@ -755,6 +770,9 @@ const currentPage = ref(1); const isFullPage = computed(() => props.result.rows.length >= pageSize.value); const isResultsContext = computed(() => props.context === "results"); const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value); +const tableUsesSyntheticRowId = computed(() => + usesSyntheticRowIdKey(props.databaseType, props.tableMeta?.primaryKeys ?? []), +); const clientSearchText = computed(() => (searchText.value.trim() ? searchText.value : "")); watch(clientSearchText, (value) => { clearTimeout(_searchTimer); @@ -826,6 +844,7 @@ const editor = useDataGridEditor({ whereFilterInput, orderByInput, rowStatusFilter, + initialEditColumn: firstVisibleColumnIndex, getRowItem, emit, }); @@ -936,6 +955,27 @@ function getRowItem(rowId: number): RowItem | undefined { return displayItems.value.find((item) => item.id === rowId); } +function visibleRowData(row: CellValue[]): CellValue[] { + return visibleColumnIndexes.value.map((index) => row[index]); +} + +function visibleDirtyColumns(row: boolean[]): boolean[] { + return visibleColumnIndexes.value.map((index) => row[index] ?? false); +} + +const visibleDisplayItems = computed(() => + displayItems.value.map((item) => ({ + ...item, + data: visibleRowData(item.data), + isDirtyCol: visibleDirtyColumns(item.isDirtyCol), + })), +); +const exportContextCell = computed(() => { + if (!contextCell.value) return null; + const visibleCol = visibleColumnIndexes.value.indexOf(contextCell.value.col); + return { ...contextCell.value, col: visibleCol }; +}); + const deleteRowDetails = computed(() => props.tableMeta?.tableName ? t("dangerDialog.deleteRowDetails", { table: props.tableMeta.tableName }) @@ -961,8 +1001,8 @@ const isErrorResult = computed( const errorMessage = computed(() => (isErrorResult.value ? String(props.result.rows[0]?.[0] ?? "") : "")); // --- Selection composable --- const selection = useDataGridSelection({ - columns: computed(() => props.result.columns), - displayItems, + columns: visibleColumns, + displayItems: visibleDisplayItems, editingCell, showTranspose, transposeRowIndex, @@ -1197,6 +1237,7 @@ async function applyOrderBySearch() { orderBy: orderByClause, limit: pageSize.value, whereInput: whereFilterInput.value.trim() || undefined, + includeRowId: tableUsesSyntheticRowId.value, }); await props.onExecuteSql(sql); } catch (e: any) { @@ -1222,6 +1263,7 @@ async function applyWhereFilter() { (sortCol.value ? `${quoteIdent(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined), limit: pageSize.value, whereInput: whereFilterInput.value.trim() || undefined, + includeRowId: tableUsesSyntheticRowId.value, }); await props.onExecuteSql(sql); } catch (e: any) { @@ -1286,8 +1328,8 @@ const { exportXlsx, copySql, } = useDataGridExport({ - columns: computed(() => props.result.columns), - displayItems, + columns: visibleColumns, + displayItems: visibleDisplayItems, sql: computed(() => props.sql), tableMeta: computed(() => props.tableMeta ? { schema: props.tableMeta.schema, tableName: props.tableMeta.tableName } : undefined, @@ -1295,8 +1337,8 @@ const { databaseType: computed(() => props.databaseType), hasCellSelection, selectedCells, - contextCell, - getRowItem, + contextCell: exportContextCell, + getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId), formatCell, quoteIdent, escapeVal, @@ -1334,9 +1376,9 @@ async function pasteClipboardIntoSelection() { const item = displayItems.value[start.rowIndex + rowOffset]; if (!item) return; row.forEach((value, colOffset) => { - const col = start.colIndex + colOffset; - if (col >= props.result.columns.length) return; - applyCellValue(item.id, col, value); + const visibleCol = start.colIndex + colOffset; + if (visibleCol >= visibleColumns.value.length) return; + applyCellValue(item.id, actualColumnIndex(visibleCol), value); }); }); toast(t("grid.pasted")); @@ -1349,8 +1391,8 @@ function cutSelection() { for (let rowIndex = range.startRow; rowIndex <= range.endRow; rowIndex++) { const item = displayItems.value[rowIndex]; if (!item) continue; - for (let col = range.startCol; col <= range.endCol; col++) { - applyCellValue(item.id, col, null); + for (let visibleCol = range.startCol; visibleCol <= range.endCol; visibleCol++) { + applyCellValue(item.id, actualColumnIndex(visibleCol), null); } } } @@ -1398,13 +1440,16 @@ const transposeData = computed(() => { if (transposeRowIndex.value === null) return null; const item = displayItems.value[transposeRowIndex.value]; if (!item) return null; - return props.result.columns.map((col, i) => ({ - column: col, - type: columnTypeMap.value.get(col) || "", - value: item.data[i], - display: formatCell(item.data[i]), - isNull: item.data[i] === null, - })); + return visibleColumnIndexes.value.map((columnIndex) => { + const col = props.result.columns[columnIndex]; + return { + column: col, + type: columnTypeMap.value.get(col) || "", + value: item.data[columnIndex], + display: formatCell(item.data[columnIndex]), + isNull: item.data[columnIndex] === null, + }; + }); }); function openTranspose(rowIndex: number) { @@ -1438,10 +1483,10 @@ watch( ); // --- Context menu handlers --- -function onCellContext(rowId: number, rowIndex: number, colIdx: number) { +function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number) { contextCell.value = { rowId, rowIndex, col: colIdx }; - if (!cellIsSelected(rowIndex, colIdx)) { - selectSingleCell(rowIndex, colIdx); + if (!cellIsSelected(rowIndex, visibleColIdx)) { + selectSingleCell(rowIndex, visibleColIdx); } } @@ -1855,44 +1900,58 @@ defineExpose({ > # - +
- {{ - col - }} + + {{ col }} + @@ -2099,24 +2158,24 @@ defineExpose({ {{ index + 1 }}
-