feat(data-grid): 支持无主键表安全编辑

- 定位策略:新增主键、唯一索引和 Oracle ROWID 三类安全行定位能力
- 查询编辑器:简单单表 SELECT 自动补充隐藏定位列,复杂结果保持只读
- 表预览:无主键表可通过唯一索引或 Oracle ROWID 安全编辑
- 提交流程:移除无主键整行 WHERE fallback,隐藏定位列不参与展示和写入
- 后端保护:Oracle、MySQL、PostgreSQL 更新删除必须恰好影响 1 行
- 测试覆盖:补充 QueryEditor、DataViewer、DataGrid 和 ApplyChanges 相关用例
Refs #419
This commit is contained in:
Syngnat
2026-04-29 12:33:35 +08:00
parent 05a913ccb2
commit b1ef52f62e
18 changed files with 1823 additions and 228 deletions

View File

@@ -0,0 +1,64 @@
export type QueryResultTableRef = {
tableName: string;
metadataDbName: string;
metadataTableName: string;
};
const stripIdentifierQuotes = (part: string): string => {
const text = String(part || '').trim();
if (!text) return '';
if ((text.startsWith('`') && text.endsWith('`')) || (text.startsWith('"') && text.endsWith('"'))) {
return text.slice(1, -1).trim();
}
if (text.startsWith('[') && text.endsWith(']')) {
return text.slice(1, -1).trim();
}
return text;
};
const normalizeQualifiedName = (raw: string): string => (
String(raw || '')
.split('.')
.map((part) => stripIdentifierQuotes(part.trim()))
.filter(Boolean)
.join('.')
);
const isOracleLikeDialect = (dialect: string): boolean => {
const normalized = String(dialect || '').trim().toLowerCase();
return normalized === 'oracle' || normalized === 'dameng' || normalized === 'dm' || normalized === 'dm8';
};
export const extractQueryResultTableRef = (
sql: string,
dialect: string,
currentDb: string,
): QueryResultTableRef | undefined => {
const text = String(sql || '').trim();
if (!text) return undefined;
if (/\b(JOIN|UNION|INTERSECT|EXCEPT|MINUS)\b/i.test(text)) return undefined;
if (/^\s*SELECT\s+DISTINCT\b/i.test(text)) return undefined;
if (/\bGROUP\s+BY\b|\bHAVING\b/i.test(text)) return undefined;
const tableMatch = text.match(/^\s*SELECT\s+.+?\s+FROM\s+((?:[`"\[]?\w+[`"\]]?)(?:\s*\.\s*(?:[`"\[]?\w+[`"\]]?)){0,2})\s*(?:$|[\s;])/im);
if (!tableMatch) return undefined;
const qualifiedName = normalizeQualifiedName(tableMatch[1]);
if (!qualifiedName) return undefined;
const parts = qualifiedName.split('.').filter(Boolean);
const metadataTableName = parts[parts.length - 1] || '';
if (!metadataTableName) return undefined;
const owner = parts.length >= 2 ? parts[parts.length - 2] : '';
const metadataDbName = owner || currentDb || '';
const tableName = isOracleLikeDialect(dialect) && owner
? `${owner}.${metadataTableName}`
: metadataTableName;
return {
tableName,
metadataDbName,
metadataTableName,
};
};