Files
MyGoNavi/frontend/src/utils/sqlErrorSemantics.ts
2026-06-22 15:12:42 +08:00

275 lines
9.2 KiB
TypeScript

export type SqlExecutionErrorFormatOptions = {
prefix?: string;
translate?: SqlExecutionErrorTranslator;
};
export type SqlExecutionErrorTranslator = (
key: string,
params?: Record<string, string | number | boolean | null | undefined>,
) => string;
type SqlErrorSemanticRule = {
key: string;
fallbackLabel: string;
fallbackExplanation: string;
fallbackSuggestion: string;
patterns: RegExp[];
};
const LOCALIZED_TIMEOUT_KEYWORDS = [
'\u8d85\u65f6',
'\u903e\u6642',
'\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8',
'zeit\u00fcberschreitung',
'\u0442\u0430\u0439\u043c-\u0430\u0443\u0442',
] as const;
const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
{
key: 'syntax',
fallbackLabel: 'SQL syntax error',
fallbackExplanation: 'Usually caused by keywords, commas, parentheses, quotes, statement order, or SQL dialect mismatch.',
fallbackSuggestion: 'Check the SQL fragment near the reported position and confirm the current data source type matches the SQL dialect.',
patterns: [
/syntax error/i,
/sql syntax/i,
/sqlstate\s*42601/i,
/near\s+["'`].+["'`]\s*:?\s*syntax error/i,
/ora-00933/i,
/ora-00936/i,
/you have an error in your sql syntax/i,
],
},
{
key: 'object_missing',
fallbackLabel: 'Table or object does not exist',
fallbackExplanation: 'The SQL references a table, view, sequence, or other database object that cannot be found in the current database or schema.',
fallbackSuggestion: 'Check the object name, casing, schema/database prefix, and whether the selected database for this query is correct.',
patterns: [
/relation\s+["'`].+["'`]\s+does not exist/i,
/table\s+.+doesn'?t exist/i,
/no such table/i,
/invalid object name/i,
/ora-00942/i,
/object\s+.+does not exist/i,
],
},
{
key: 'column_missing',
fallbackLabel: 'Column does not exist',
fallbackExplanation: 'The SQL references a column that is not in the result set, is spelled differently, or does not exist on the current table.',
fallbackSuggestion: 'Check column names, aliases, casing, table aliases, and whether the column belongs to the current FROM/JOIN object.',
patterns: [
/column\s+["'`].+["'`]\s+does not exist/i,
/unknown column/i,
/invalid column name/i,
/ora-00904/i,
/no such column/i,
],
},
{
key: 'unique_conflict',
fallbackLabel: 'Unique constraint or primary key conflict',
fallbackExplanation: 'The inserted or updated data duplicates an existing value in a unique index, primary key, or unique constraint.',
fallbackSuggestion: 'Check the duplicate key value and use UPDATE or UPSERT if appropriate, or adjust the unique-key field value.',
patterns: [
/duplicate key/i,
/duplicate entry/i,
/unique constraint failed/i,
/violates unique constraint/i,
/ora-00001/i,
],
},
{
key: 'permission_denied',
fallbackLabel: 'Insufficient permissions',
fallbackExplanation: 'The current database account does not have permission to execute this SQL or access the related objects.',
fallbackSuggestion: 'Check account privileges, schema grants, read-only connection limits, and whether an administrator needs to grant access.',
patterns: [
/permission denied/i,
/access denied/i,
/not authorized/i,
/insufficient privileges/i,
/ora-01031/i,
],
},
{
key: 'type_mismatch',
fallbackLabel: 'Data type or format mismatch',
fallbackExplanation: 'The value being written, compared, or converted does not match the target column or expression format.',
fallbackSuggestion: 'Check dates, numbers, booleans, enum values, implicit casts, and column types; use an explicit CAST if needed.',
patterns: [
/invalid input syntax/i,
/incorrect\s+.+\s+value/i,
/data truncated/i,
/truncated incorrect/i,
/conversion failed/i,
/invalid number/i,
/ora-01722/i,
],
},
{
key: 'constraint_failed',
fallbackLabel: 'Constraint validation failed',
fallbackExplanation: 'The data violates a foreign key, non-null, check constraint, or referential integrity rule.',
fallbackSuggestion: 'Check related parent records, required fields, CHECK conditions, and whether the write order is correct.',
patterns: [
/foreign key constraint/i,
/violates foreign key constraint/i,
/cannot be null/i,
/not null constraint failed/i,
/check constraint/i,
/constraint failed/i,
],
},
{
key: 'timeout_or_canceled',
fallbackLabel: 'Query timed out or was canceled',
fallbackExplanation: 'The SQL ran longer than the timeout limit, or execution was manually canceled.',
fallbackSuggestion: 'Check the SQL execution plan, filters, and indexes; narrow the query range or adjust the timeout if needed.',
patterns: [
/context deadline exceeded/i,
/statement canceled/i,
/statement cancelled/i,
/context canceled/i,
/context cancelled/i,
/timeout/i,
/timed out/i,
],
},
{
key: 'connection_or_auth',
fallbackLabel: 'Database connection or authentication failed',
fallbackExplanation: 'The client could not connect to the database, or credentials, network, or instance state may be wrong.',
fallbackSuggestion: 'Check host, port, username, password, network reachability, proxy/SSH tunnel, and database service status.',
patterns: [
/password authentication failed/i,
/connection refused/i,
/no route to host/i,
/server has gone away/i,
/too many connections/i,
/connection reset/i,
/connection timeout/i,
],
},
];
const GENERIC_SQL_ERROR_RULE: SqlErrorSemanticRule = {
key: 'generic',
fallbackLabel: 'Database execution error',
fallbackExplanation: 'The database returned an execution failure, and no more specific error type was matched.',
fallbackSuggestion: 'Continue troubleshooting with the raw error, SQL fragment, and current database dialect.',
patterns: [],
};
const LEGACY_SEMANTIC_PREFIX = '\u4e2d\u6587\u8bed\u4e49\uff1a';
const LEGACY_RAW_PREFIX = '\u539f\u59cb\u9519\u8bef\uff1a';
const normalizeErrorText = (raw: unknown): string => {
if (raw instanceof Error) {
return raw.message || String(raw);
}
if (typeof raw === 'string') {
return raw;
}
if (raw == null) {
return '';
}
try {
return JSON.stringify(raw);
} catch {
return String(raw);
}
};
const includesLocalizedKeyword = (
message: string,
keywords: readonly string[],
): boolean => {
const lower = String(message || '').toLowerCase();
return keywords.some((keyword) => lower.includes(keyword.toLowerCase()));
};
export const hasLocalizedSqlTimeoutKeyword = (message: string): boolean =>
includesLocalizedKeyword(message, LOCALIZED_TIMEOUT_KEYWORDS);
const findSqlErrorSemantic = (message: string): SqlErrorSemanticRule | null => {
const text = String(message || '');
const matchedRule = SQL_ERROR_RULES.find((rule) => rule.patterns.some((pattern) => pattern.test(text)));
if (matchedRule) {
return matchedRule;
}
if (hasLocalizedSqlTimeoutKeyword(text)) {
return SQL_ERROR_RULES.find((rule) => rule.key === 'timeout_or_canceled') || null;
}
return null;
};
const translateSqlErrorCopy = (
translate: SqlExecutionErrorTranslator | undefined,
key: string,
fallback: string,
params?: Record<string, string | number | boolean | null | undefined>,
): string => {
if (!translate) {
return fallback;
}
const translated = translate(key, params);
return translated && translated !== key ? translated : fallback;
};
const localizeRule = (
rule: SqlErrorSemanticRule,
translate?: SqlExecutionErrorTranslator,
) => {
const baseKey = `query_editor.sql_error.rule.${rule.key}`;
return {
label: translateSqlErrorCopy(translate, `${baseKey}.label`, rule.fallbackLabel),
explanation: translateSqlErrorCopy(translate, `${baseKey}.explanation`, rule.fallbackExplanation),
suggestion: translateSqlErrorCopy(translate, `${baseKey}.suggestion`, rule.fallbackSuggestion),
};
};
export const formatSqlExecutionError = (
raw: unknown,
options: SqlExecutionErrorFormatOptions = {},
): string => {
const translate = options.translate;
const rawMessage = normalizeErrorText(raw).trim() || translateSqlErrorCopy(
translate,
'query_editor.sql_error.unknown',
'Unknown error',
);
if (rawMessage.includes(LEGACY_SEMANTIC_PREFIX) && rawMessage.includes(LEGACY_RAW_PREFIX)) {
return rawMessage;
}
const semantic = localizeRule(findSqlErrorSemantic(rawMessage) || GENERIC_SQL_ERROR_RULE, translate);
const prefix = String(options.prefix || '').trim();
return [
prefix,
translateSqlErrorCopy(
translate,
'query_editor.sql_error.wrapper.semantic_line',
`Semantic meaning: ${semantic.label}. ${semantic.explanation}`,
{
label: semantic.label,
explanation: semantic.explanation,
},
),
translateSqlErrorCopy(
translate,
'query_editor.sql_error.wrapper.suggestion_line',
`Suggestion: ${semantic.suggestion}`,
{ suggestion: semantic.suggestion },
),
translateSqlErrorCopy(
translate,
'query_editor.sql_error.wrapper.raw_line',
`Raw error: ${rawMessage}`,
{ error: rawMessage },
),
].filter(Boolean).join('\n');
};