fix(hive): allow non-transactional grid edits

This commit is contained in:
t8y2 2026-05-13 20:02:36 +08:00
parent 3eb00b7fe2
commit ea6b2bb36c
6 changed files with 76 additions and 6 deletions

View File

@ -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"

View File

@ -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;
}

View File

@ -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)];

View File

@ -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 &&

View File

@ -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"],

View File

@ -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]);
});