fix(GOV-012): 兼容 MySQL 5.7 视图目录并封存 v0.0.9 基线
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user