mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-13 17:29:46 +08:00
- 新增 RedisMonitor 面板,展示 QPS、内存、CPU、连接数和键数量趋势图 - 引入 recharts 依赖并补齐监控图表所需前端包与锁文件 - Sidebar 新增 Redis 实例监控入口,TabManager 与 TabData 接入 redis-monitor 页签类型 - RedisCommandEditor 支持多行脚本块解析、选区执行、耗时记录与终端化结果展示 - Oracle 表预览移除自动精确 COUNT(*),避免打开大表时额外阻塞 - 无筛选整表预览接入 ALL_TABLES.NUM_ROWS 近似总数展示,并拆分近似总数与近似总页数语义
107 lines
4.1 KiB
TypeScript
107 lines
4.1 KiB
TypeScript
export type ApproximateTableCountStrategy = 'none' | 'duckdb-estimated-size' | 'oracle-num-rows';
|
|
|
|
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
|
|
const toNonNegativeFiniteNumber = (value: unknown): number | null => {
|
|
if (typeof value === 'number') {
|
|
return Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER ? value : null;
|
|
}
|
|
if (typeof value === 'bigint') {
|
|
return value >= 0n && value <= MAX_SAFE_BIGINT ? Number(value) : null;
|
|
}
|
|
if (typeof value === 'string') {
|
|
const text = value.trim();
|
|
if (!text) return null;
|
|
if (/^[+-]?\d+$/.test(text)) {
|
|
try {
|
|
const parsed = BigInt(text);
|
|
return parsed >= 0n && parsed <= MAX_SAFE_BIGINT ? Number(parsed) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
const parsed = Number(text);
|
|
return Number.isFinite(parsed) && parsed >= 0 && parsed <= Number.MAX_SAFE_INTEGER ? parsed : null;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const stripOuterQuotes = (value: string): string => {
|
|
const trimmed = String(value || '').trim();
|
|
if (trimmed.length < 2) return trimmed;
|
|
const first = trimmed[0];
|
|
const last = trimmed[trimmed.length - 1];
|
|
if ((first === '"' && last === '"') || (first === '`' && last === '`') || (first === '[' && last === ']')) {
|
|
return trimmed.slice(1, -1).trim();
|
|
}
|
|
return trimmed;
|
|
};
|
|
|
|
const escapeSQLLiteral = (value: string): string => String(value || '').replace(/'/g, "''");
|
|
|
|
const resolveOracleOwnerAndTable = (params: { dbName: string; tableName: string }) => {
|
|
const rawTable = String(params.tableName || '').trim();
|
|
const parts = rawTable.split('.').map(stripOuterQuotes).filter(Boolean);
|
|
const tableName = String(parts[parts.length - 1] || rawTable || '').trim();
|
|
const ownerCandidate = parts.length >= 2 ? parts[parts.length - 2] : String(params.dbName || '').trim();
|
|
return {
|
|
owner: ownerCandidate.toUpperCase(),
|
|
tableName: tableName.toUpperCase(),
|
|
};
|
|
};
|
|
|
|
export const resolveApproximateTableCountStrategy = (params: {
|
|
dbType: string;
|
|
whereSQL: string;
|
|
}): ApproximateTableCountStrategy => {
|
|
const dbType = String(params.dbType || '').trim().toLowerCase();
|
|
const whereSQL = String(params.whereSQL || '').trim();
|
|
if (whereSQL) return 'none';
|
|
if (dbType === 'duckdb') return 'duckdb-estimated-size';
|
|
if (dbType === 'oracle') return 'oracle-num-rows';
|
|
return 'none';
|
|
};
|
|
|
|
export const buildOracleApproximateTotalSql = (params: { dbName: string; tableName: string }): string => {
|
|
const { owner, tableName } = resolveOracleOwnerAndTable(params);
|
|
const escapedTable = escapeSQLLiteral(tableName);
|
|
if (!owner) {
|
|
return `SELECT num_rows AS approx_total FROM user_tables WHERE table_name = '${escapedTable}' AND ROWNUM = 1`;
|
|
}
|
|
return `SELECT num_rows AS approx_total FROM all_tables WHERE owner = '${escapeSQLLiteral(owner)}' AND table_name = '${escapedTable}' AND ROWNUM = 1`;
|
|
};
|
|
|
|
export const parseApproximateTableCountRow = (
|
|
row: unknown,
|
|
preferredKeys: string[] = ['approx_total', 'estimated_size', 'estimated_rows', 'row_count', 'num_rows', 'count', 'total'],
|
|
): number | null => {
|
|
if (!row || typeof row !== 'object') return null;
|
|
const entries = Object.entries(row as Record<string, unknown>);
|
|
if (entries.length === 0) return null;
|
|
|
|
for (const preferredKey of preferredKeys) {
|
|
const normalizedPreferred = String(preferredKey || '').trim().toLowerCase();
|
|
for (const [key, value] of entries) {
|
|
if (String(key || '').trim().toLowerCase() !== normalizedPreferred) continue;
|
|
const parsed = toNonNegativeFiniteNumber(value);
|
|
if (parsed !== null) return parsed;
|
|
}
|
|
}
|
|
|
|
for (const [key, value] of entries) {
|
|
const normalizedKey = String(key || '').trim().toLowerCase();
|
|
if (!normalizedKey.includes('estimate') && !normalizedKey.includes('row') && !normalizedKey.includes('count') && !normalizedKey.includes('total')) {
|
|
continue;
|
|
}
|
|
const parsed = toNonNegativeFiniteNumber(value);
|
|
if (parsed !== null) return parsed;
|
|
}
|
|
|
|
for (const [, value] of entries) {
|
|
const parsed = toNonNegativeFiniteNumber(value);
|
|
if (parsed !== null) return parsed;
|
|
}
|
|
|
|
return null;
|
|
};
|