From ea6b2bb36c28024008b9440f21abc91fff1346c6 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 13 May 2026 20:02:36 +0800 Subject: [PATCH] fix(hive): allow non-transactional grid edits --- src/components/layout/ContentArea.vue | 3 ++- src/composables/useDataGridEditor.ts | 15 ++++++++---- src/lib/dataGridSql.ts | 9 ++++++++ src/lib/tableEditing.ts | 9 ++++++++ tests/dataGridSql.test.ts | 33 +++++++++++++++++++++++++++ tests/tableEditing.test.ts | 13 +++++++++++ 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/components/layout/ContentArea.vue b/src/components/layout/ContentArea.vue index c55b52e42..44b5cf70f 100644 --- a/src/components/layout/ContentArea.vue +++ b/src/components/layout/ContentArea.vue @@ -17,6 +17,7 @@ const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryCh import { useQueryStore } from "@/stores/queryStore"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState"; import { databaseDisplayNameForTab } from "@/lib/tabPresentation"; +import { isTableDataEditable } from "@/lib/tableEditing"; import type { QueryTab, ConnectionConfig } from "@/types/database"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; @@ -390,7 +391,7 @@ defineExpose({ focusSearch }); :result="activeTab.result" :sql="activeTab.sql" :loading="activeTab.isExecuting" - :editable="!!activeTab.tableMeta?.primaryKeys?.length" + :editable="isTableDataEditable(activeConnection?.db_type, activeTab.tableMeta?.primaryKeys ?? [])" context="table-data" :initial-where-input="activeTab.whereInput" :database-type="activeConnection?.db_type" diff --git a/src/composables/useDataGridEditor.ts b/src/composables/useDataGridEditor.ts index 264358a0e..ef668e866 100644 --- a/src/composables/useDataGridEditor.ts +++ b/src/composables/useDataGridEditor.ts @@ -4,9 +4,11 @@ import { buildDataGridRollbackStatements, buildDataGridSaveStatements, dataGridSaveExecutionSchema, + normalizeDataGridSaveError, validateDataGridSave, } from "@/lib/dataGridSql"; import { rowStatusFilterAfterAddingRow, type RowStatusFilter } from "@/lib/gridRowStatus"; +import { supportsDataGridTransaction } from "@/lib/tableEditing"; import { useConnectionStore } from "@/stores/connectionStore"; import { useHistoryStore } from "@/stores/historyStore"; import type { ColumnInfo, DatabaseType } from "@/types/database"; @@ -125,7 +127,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { const saveError = ref(""); const useTransaction = computed( - () => editable.value && (!!customSave?.value || (!!connectionId.value && !!database.value && !!tableMeta.value)), + () => + editable.value && + supportsDataGridTransaction(databaseType.value) && + (!!customSave?.value || (!!connectionId.value && !!database.value && !!tableMeta.value)), ); function enterTransaction() { @@ -536,7 +541,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { rows: result.value.rows, }); } catch (e: any) { - saveError.value = String(e.message || e); + saveError.value = normalizeDataGridSaveError(databaseType.value, e); isSaving.value = false; return; } @@ -599,7 +604,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { dataGridSaveExecutionSchema(databaseType.value, tableMeta.value), ); } catch (e: any) { - saveError.value = String(e.message || e); + saveError.value = normalizeDataGridSaveError(databaseType.value, e); isSaving.value = false; return; } @@ -607,7 +612,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { try { apiResult = await api.executeBatch(connectionId.value, database.value, stmts); } catch (e: any) { - saveError.value = String(e.message || e); + saveError.value = normalizeDataGridSaveError(databaseType.value, e); isSaving.value = false; return; } @@ -617,7 +622,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { await onExecuteSql.value(sqlStmt); } } catch (e: any) { - saveError.value = String(e.message || e); + saveError.value = normalizeDataGridSaveError(databaseType.value, e); isSaving.value = false; return; } diff --git a/src/lib/dataGridSql.ts b/src/lib/dataGridSql.ts index 150917702..69a1a23d9 100644 --- a/src/lib/dataGridSql.ts +++ b/src/lib/dataGridSql.ts @@ -181,6 +181,14 @@ export function dataGridSaveExecutionSchema( return tableMeta?.schema; } +export function normalizeDataGridSaveError(databaseType: DatabaseType | undefined, error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + if (databaseType === "hive" && /Attempt to do update or delete|Error 10294/i.test(message)) { + return "Hive UPDATE/DELETE are not enabled for this table or server. Add rows with INSERT, or enable ACID transactional tables in Hive before editing/deleting existing rows."; + } + return message; +} + export function formatGridSqlLiteral(value: GridCellValue, databaseType?: DatabaseType): string { if (value === null || value === undefined) return "NULL"; if (typeof value === "boolean") return value ? "TRUE" : "FALSE"; @@ -197,6 +205,7 @@ function buildPrimaryKeyWhere( columns: string[], row: GridCellValue[], ): string { + if (databaseType === "hive" && primaryKeys.length === 0) return buildRowWhere(databaseType, columns, row); return primaryKeys .map((primaryKey) => { const value = row[columns.indexOf(primaryKey)]; diff --git a/src/lib/tableEditing.ts b/src/lib/tableEditing.ts index e1a7d3cb0..68b9f185f 100644 --- a/src/lib/tableEditing.ts +++ b/src/lib/tableEditing.ts @@ -10,6 +10,15 @@ export function editablePrimaryKeys(databaseType: DatabaseType | undefined, colu return primaryKeys; } +export function isTableDataEditable(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean { + if (databaseType === "hive") return true; + return primaryKeys.length > 0; +} + +export function supportsDataGridTransaction(databaseType: DatabaseType | undefined): boolean { + return databaseType !== "hive"; +} + export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean { return ( primaryKeys.length === 1 && diff --git a/tests/dataGridSql.test.ts b/tests/dataGridSql.test.ts index 7123487e6..0b673cfee 100644 --- a/tests/dataGridSql.test.ts +++ b/tests/dataGridSql.test.ts @@ -4,6 +4,7 @@ import { buildDataGridRollbackStatements, buildDataGridSaveStatements, dataGridSaveExecutionSchema, + normalizeDataGridSaveError, validateDataGridSave, } from "../src/lib/dataGridSql.ts"; import { DBX_NEO4J_ELEMENT_ID_COLUMN } from "../src/lib/tableEditing.ts"; @@ -59,6 +60,26 @@ test("builds Hive grid save statements with backtick identifiers", () => { ]); }); +test("builds Hive grid save statements without primary keys using row predicates", () => { + const statements = buildDataGridSaveStatements({ + databaseType: "hive", + tableMeta: { + tableName: "departments", + primaryKeys: [], + }, + columns: ["id", "name", "location"], + rows: [[10, "Sales", null]], + dirtyRows: [[0, [[1, "Marketing"]]]], + deletedRows: [0], + newRows: [], + }); + + assert.deepEqual(statements, [ + "UPDATE `departments` SET `name` = 'Marketing' WHERE `id` = 10 AND `name` = 'Sales' AND `location` IS NULL;", + "DELETE FROM `departments` WHERE `id` = 10 AND `name` = 'Sales' AND `location` IS NULL;", + ]); +}); + test("uses Oracle ROWID as a synthetic key without writing it as a normal column", () => { const statements = buildDataGridSaveStatements({ databaseType: "oracle", @@ -141,6 +162,18 @@ test("skips current_schema setup for Oracle data grid saves", () => { ); }); +test("normalizes Hive ACID update and delete errors", () => { + const error = normalizeDataGridSaveError( + "hive", + "Statement 1 failed: Agent RPC error (-1): Error while compiling statement: FAILED: SemanticException [Error 10294]: Attempt to do update or delete using transaction manager that does not support these operations.. Previous 0 statement(s) may have been committed.", + ); + + assert.equal( + error, + "Hive UPDATE/DELETE are not enabled for this table or server. Add rows with INSERT, or enable ACID transactional tables in Hive before editing/deleting existing rows.", + ); +}); + test("rejects NULL writes to non-null table columns", () => { const error = validateDataGridSave({ columns: ["ID", "CREATED_AT", "CITY"], diff --git a/tests/tableEditing.test.ts b/tests/tableEditing.test.ts index 416c85bf1..da23684ad 100644 --- a/tests/tableEditing.test.ts +++ b/tests/tableEditing.test.ts @@ -5,6 +5,8 @@ import { DBX_ROWID_COLUMN, editablePrimaryKeys, isHiddenGridColumn, + isTableDataEditable, + supportsDataGridTransaction, usesSyntheticRowIdKey, } from "../src/lib/tableEditing.ts"; import type { ColumnInfo } from "../src/types/database.ts"; @@ -32,6 +34,17 @@ test("does not synthesize ROWID for non-Oracle keyless tables", () => { assert.deepEqual(editablePrimaryKeys("mysql", [column("ID"), column("CITY")]), []); }); +test("allows Hive table data editing even without declared primary keys", () => { + assert.equal(isTableDataEditable("hive", []), true); + assert.equal(isTableDataEditable("mysql", []), false); + assert.equal(isTableDataEditable("postgres", ["id"]), true); +}); + +test("does not use transactional grid saves for Hive", () => { + assert.equal(supportsDataGridTransaction("hive"), false); + assert.equal(supportsDataGridTransaction("postgres"), true); +}); + test("uses elementId as Neo4j editable key when labels have no primary key", () => { assert.deepEqual(editablePrimaryKeys("neo4j", [column("name"), column("role")]), [DBX_NEO4J_ELEMENT_ID_COLUMN]); });