fix(GOV-012): 兼容 MySQL 5.7 视图目录并封存 v0.0.9 基线

This commit is contained in:
2026-08-18 02:57:40 +08:00
parent 874796ab65
commit afd186c8a4
4 changed files with 146 additions and 15 deletions
+69 -14
View File
@@ -19,6 +19,10 @@ ORDER BY TABLE_NAME`,
FROM information_schema.VIEW_TABLE_USAGE
WHERE VIEW_SCHEMA = ?
ORDER BY VIEW_NAME, REFERENCED_SCHEMA_NAME, REFERENCED_TABLE_NAME`,
viewDefinitions: `SELECT TABLE_NAME AS VIEW_NAME, VIEW_DEFINITION
FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = ?
ORDER BY TABLE_NAME`,
routines: `SELECT ROUTINE_NAME, ROUTINE_TYPE
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = ?
@@ -128,7 +132,7 @@ function requireCatalogText(value) {
return value;
}
async function queryCatalogRows(connection, sql, database, limits) {
async function queryCatalogRows(connection, sql, database, limits, options = {}) {
let result;
try {
result = await connection.query({
@@ -137,6 +141,9 @@ async function queryCatalogRows(connection, sql, database, limits) {
timeout: limits.queryTimeoutMs,
});
} catch (error) {
if (options.allowMissingTable && error?.code === 'ER_UNKNOWN_TABLE') {
throw new SchemaExportError('OPTIONAL_CATALOG_UNAVAILABLE', '可选 Schema 目录来源不可用');
}
// mysql2 错误可能包含连接 URL、用户名或密码,目录读取的公共接口一律不透传原始文本。
sanitizeDatabaseQueryError(error);
}
@@ -146,6 +153,38 @@ async function queryCatalogRows(connection, sql, database, limits) {
return result[0];
}
function unquoteViewIdentifier(identifier) {
const value = identifier.startsWith('`') && identifier.endsWith('`')
? identifier.slice(1, -1).replaceAll('``', '`')
: identifier;
return value;
}
function parseViewDependencies(viewRows, database) {
const dependencies = [];
for (const row of viewRows) {
const view = requireCatalogText(row.VIEW_NAME);
const definition = requireCatalogText(row.VIEW_DEFINITION);
const inspectable = maskStringLiterals(definition);
const referencePattern = /\b(?:FROM|JOIN)\s+((?:`[^`]+`|[A-Za-z0-9_$]+)(?:\s*\.\s*(?:`[^`]+`|[A-Za-z0-9_$]+))?)/gi;
for (const match of inspectable.matchAll(referencePattern)) {
const parts = match[1].split(/\s*\.\s*/).map(unquoteViewIdentifier);
dependencies.push({
view,
referencedSchema: parts.length === 2 ? parts[0] : database,
referencedObject: parts.length === 2 ? parts[1] : parts[0],
});
}
}
dependencies.sort(
(left, right) =>
left.view.localeCompare(right.view) ||
left.referencedSchema.localeCompare(right.referencedSchema) ||
left.referencedObject.localeCompare(right.referencedObject),
);
return dependencies;
}
function assertObjectLimit(count, limits) {
if (!Number.isInteger(limits.maxObjects) || limits.maxObjects < 0 || count > limits.maxObjects) {
throw new SchemaExportError('OBJECT_LIMIT_EXCEEDED', 'Schema 对象数量超过安全上限');
@@ -171,17 +210,30 @@ export async function readSchemaCatalog(connection, database, limits = DEFAULT_E
let objectCount = tables.length + views.length;
assertObjectLimit(objectCount, limits);
const dependencyRows = await queryCatalogRows(
connection,
CATALOG_QUERIES.viewDependencies,
database,
limits,
);
const viewDependencies = dependencyRows.map((row) => ({
view: requireCatalogText(row.VIEW_NAME),
referencedSchema: requireCatalogText(row.REFERENCED_SCHEMA_NAME),
referencedObject: requireCatalogText(row.REFERENCED_TABLE_NAME),
}));
let viewDependencies;
try {
const dependencyRows = await queryCatalogRows(
connection,
CATALOG_QUERIES.viewDependencies,
database,
limits,
{ allowMissingTable: true },
);
viewDependencies = dependencyRows.map((row) => ({
view: requireCatalogText(row.VIEW_NAME),
referencedSchema: requireCatalogText(row.REFERENCED_SCHEMA_NAME),
referencedObject: requireCatalogText(row.REFERENCED_TABLE_NAME),
}));
} catch (error) {
if (error.code !== 'OPTIONAL_CATALOG_UNAVAILABLE') throw error;
const viewRows = await queryCatalogRows(
connection,
CATALOG_QUERIES.viewDefinitions,
database,
limits,
);
viewDependencies = parseViewDependencies(viewRows, database);
}
const routineRows = await queryCatalogRows(connection, CATALOG_QUERIES.routines, database, limits);
const routines = routineRows.map((row) => {
@@ -518,6 +570,7 @@ export async function exportMysql57Schema({
);
}
assertWithinDeadline(startedAt, now, limits);
const objectCounts = buildObjectCounts(beforeCatalog);
result = Object.freeze({
metadata: Object.freeze({
exporterName: MYSQL2_EXPORTER_METADATA.name,
@@ -526,8 +579,10 @@ export async function exportMysql57Schema({
driverVersion: MYSQL2_EXPORTER_METADATA.driverVersion,
...serverMetadata,
}),
rawSql: renderSchemaSql(beforeCatalog, beforeDefinitions),
objectCounts: buildObjectCounts(beforeCatalog),
rawSql: Object.values(objectCounts).every((count) => count === 0)
? ''
: renderSchemaSql(beforeCatalog, beforeDefinitions),
objectCounts,
durationMs: now() - startedAt,
});
} catch (error) {