🐛 fix(duckdb): 修复无主键结果无法安全编辑

- 为 DuckDB 查询结果和表预览补充隐藏 rowid 定位列,允许无主键表安全提交修改
- DataGrid 提交变更时仅将 rowid 用作定位条件,避免把隐藏定位列写回业务字段
- DuckDB ApplyChanges 对 duckdb-rowid 改用未加引号的 rowid 条件,修复更新和删除失效
- 补充前后端回归测试,覆盖 QueryEditor、DataViewer、rowLocator 与 ApplyChanges 链路
This commit is contained in:
Syngnat
2026-06-05 14:05:18 +08:00
parent ea1737ab8d
commit 2254b76232
9 changed files with 378 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
DUCKDB_ROWID_LOCATOR_COLUMN,
ORACLE_ROWID_LOCATOR_COLUMN,
filterHiddenLocatorColumns,
resolveEditRowLocator,
@@ -103,6 +104,20 @@ describe('resolveEditRowLocator', () => {
readOnly: false,
});
});
it('uses DuckDB rowid when no primary or unique key is available', () => {
expect(resolveEditRowLocator({
dbType: 'duckdb',
resultColumns: ['name', DUCKDB_ROWID_LOCATOR_COLUMN],
allowDuckDBRowID: true,
})).toEqual({
strategy: 'duckdb-rowid',
columns: ['rowid'],
valueColumns: [DUCKDB_ROWID_LOCATOR_COLUMN],
hiddenColumns: [DUCKDB_ROWID_LOCATOR_COLUMN],
readOnly: false,
});
});
});
describe('resolveRowLocatorValues', () => {
@@ -131,6 +146,19 @@ describe('resolveRowLocatorValues', () => {
error: '定位列 EMAIL 的值为空,无法安全提交修改。',
});
});
it('extracts DuckDB rowid locator values from the original row', () => {
const locator = resolveEditRowLocator({
dbType: 'duckdb',
resultColumns: ['name', DUCKDB_ROWID_LOCATOR_COLUMN],
allowDuckDBRowID: true,
});
expect(resolveRowLocatorValues(locator, { name: 'launch', [DUCKDB_ROWID_LOCATOR_COLUMN]: 17 })).toEqual({
ok: true,
values: { rowid: 17 },
});
});
});
describe('filterHiddenLocatorColumns', () => {
@@ -143,4 +171,14 @@ describe('filterHiddenLocatorColumns', () => {
expect(filterHiddenLocatorColumns(['NAME', ORACLE_ROWID_LOCATOR_COLUMN], locator)).toEqual(['NAME']);
});
it('removes hidden DuckDB rowid columns from displayed columns', () => {
const locator = resolveEditRowLocator({
dbType: 'duckdb',
resultColumns: ['name', DUCKDB_ROWID_LOCATOR_COLUMN],
allowDuckDBRowID: true,
});
expect(filterHiddenLocatorColumns(['name', DUCKDB_ROWID_LOCATOR_COLUMN], locator)).toEqual(['name']);
});
});

View File

@@ -3,8 +3,9 @@ import { resolveUniqueKeyGroupsFromIndexes } from '../components/dataGridCopyIns
import { isOracleLikeDialect } from './sqlDialect';
export const ORACLE_ROWID_LOCATOR_COLUMN = '__gonavi_oracle_rowid__';
export const DUCKDB_ROWID_LOCATOR_COLUMN = '__gonavi_duckdb_rowid__';
export type RowLocatorStrategy = 'primary-key' | 'unique-key' | 'oracle-rowid' | 'none';
export type RowLocatorStrategy = 'primary-key' | 'unique-key' | 'oracle-rowid' | 'duckdb-rowid' | 'none';
export type EditRowLocator = {
strategy: RowLocatorStrategy;
@@ -22,6 +23,7 @@ export type ResolveEditRowLocatorParams = {
primaryKeys?: string[];
indexes?: IndexDefinition[];
allowOracleRowID?: boolean;
allowDuckDBRowID?: boolean;
};
export type ResolveRowLocatorValuesResult =
@@ -54,6 +56,7 @@ export const resolveEditRowLocator = ({
primaryKeys = [],
indexes,
allowOracleRowID = false,
allowDuckDBRowID = false,
}: ResolveEditRowLocatorParams): EditRowLocator => {
const columns = (resultColumns || []).map(normalizeColumnName).filter(Boolean);
const primaryKeyColumns = (primaryKeys || []).map(normalizeColumnName).filter(Boolean);
@@ -93,10 +96,25 @@ export const resolveEditRowLocator = ({
};
}
if (allowDuckDBRowID && String(dbType || '').trim().toLowerCase() === 'duckdb' && hasColumn(columns, DUCKDB_ROWID_LOCATOR_COLUMN)) {
const rowIDColumn = findColumn(columns, DUCKDB_ROWID_LOCATOR_COLUMN);
return {
strategy: 'duckdb-rowid',
columns: ['rowid'],
valueColumns: [rowIDColumn],
hiddenColumns: [rowIDColumn],
readOnly: false,
};
}
if (allowOracleRowID && isOracleLikeDialect(dbType)) {
return buildReadOnlyLocator('未检测到主键或可用唯一索引,且结果中缺少 Oracle ROWID无法安全提交修改。');
}
if (allowDuckDBRowID && String(dbType || '').trim().toLowerCase() === 'duckdb') {
return buildReadOnlyLocator('未检测到主键、可用唯一索引或 DuckDB rowid无法安全提交修改。');
}
return buildReadOnlyLocator('未检测到主键或可用唯一索引,无法安全提交修改。');
};