feat(GOV-012): 实现 Schema 对象目录与视图排序
This commit is contained in:
@@ -7,7 +7,9 @@ import {
|
||||
SchemaExportError,
|
||||
assertMysql57ServerMetadata,
|
||||
buildConnectionOptions,
|
||||
orderViewNames,
|
||||
quoteIdentifier,
|
||||
readSchemaCatalog,
|
||||
} from './mysql2-schema-exporter.mjs';
|
||||
|
||||
const config = Object.freeze({
|
||||
@@ -57,3 +59,170 @@ test('只接受 MySQL 5.7.x 与固定字符规则', () => {
|
||||
assert.throws(() => assertMysql57ServerMetadata({ ...valid, serverVersion }));
|
||||
}
|
||||
});
|
||||
|
||||
function createCatalogConnection(overrides = {}) {
|
||||
const rowsBySource = {
|
||||
'information_schema.TABLES': [
|
||||
{ TABLE_NAME: 'market_snapshot', TABLE_TYPE: 'BASE TABLE' },
|
||||
{ TABLE_NAME: 'market_dashboard', TABLE_TYPE: 'VIEW' },
|
||||
{ TABLE_NAME: 'market_summary', TABLE_TYPE: 'VIEW' },
|
||||
],
|
||||
'information_schema.VIEW_TABLE_USAGE': [
|
||||
{
|
||||
VIEW_NAME: 'market_dashboard',
|
||||
REFERENCED_SCHEMA_NAME: 'a_share_analysis_dev',
|
||||
REFERENCED_TABLE_NAME: 'market_summary',
|
||||
},
|
||||
{
|
||||
VIEW_NAME: 'market_summary',
|
||||
REFERENCED_SCHEMA_NAME: 'a_share_analysis_dev',
|
||||
REFERENCED_TABLE_NAME: 'market_snapshot',
|
||||
},
|
||||
],
|
||||
'information_schema.ROUTINES': [
|
||||
{ ROUTINE_NAME: 'calculate_breadth', ROUTINE_TYPE: 'FUNCTION' },
|
||||
{ ROUTINE_NAME: 'refresh_market_snapshot', ROUTINE_TYPE: 'PROCEDURE' },
|
||||
],
|
||||
'information_schema.TRIGGERS': [
|
||||
{
|
||||
TRIGGER_NAME: 'snapshot_before_insert',
|
||||
EVENT_OBJECT_TABLE: 'market_snapshot',
|
||||
ACTION_TIMING: 'BEFORE',
|
||||
EVENT_MANIPULATION: 'INSERT',
|
||||
ACTION_ORDER: 1,
|
||||
},
|
||||
],
|
||||
'information_schema.EVENTS': [{ EVENT_NAME: 'purge_market_snapshot' }],
|
||||
...overrides,
|
||||
};
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
async query(options) {
|
||||
calls.push(options);
|
||||
const source = Object.keys(rowsBySource).find((candidate) => options.sql.includes(candidate));
|
||||
assert.ok(source, `未识别的目录查询:${options.sql}`);
|
||||
return [rowsBySource[source], []];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('目录查询使用固定参数和超时并返回稳定对象', async () => {
|
||||
const connection = createCatalogConnection();
|
||||
const catalog = await readSchemaCatalog(connection, 'a_share_analysis_dev', DEFAULT_EXPORT_LIMITS);
|
||||
|
||||
assert.deepEqual(catalog, {
|
||||
tables: ['market_snapshot'],
|
||||
views: ['market_dashboard', 'market_summary'],
|
||||
viewDependencies: [
|
||||
{
|
||||
view: 'market_dashboard',
|
||||
referencedSchema: 'a_share_analysis_dev',
|
||||
referencedObject: 'market_summary',
|
||||
},
|
||||
{
|
||||
view: 'market_summary',
|
||||
referencedSchema: 'a_share_analysis_dev',
|
||||
referencedObject: 'market_snapshot',
|
||||
},
|
||||
],
|
||||
routines: [
|
||||
{ name: 'calculate_breadth', type: 'FUNCTION' },
|
||||
{ name: 'refresh_market_snapshot', type: 'PROCEDURE' },
|
||||
],
|
||||
triggers: [
|
||||
{
|
||||
name: 'snapshot_before_insert',
|
||||
table: 'market_snapshot',
|
||||
timing: 'BEFORE',
|
||||
event: 'INSERT',
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
events: ['purge_market_snapshot'],
|
||||
});
|
||||
assert.equal(connection.calls.length, 5);
|
||||
for (const call of connection.calls) {
|
||||
assert.deepEqual(call.values, ['a_share_analysis_dev']);
|
||||
assert.equal(call.timeout, DEFAULT_EXPORT_LIMITS.queryTimeoutMs);
|
||||
}
|
||||
});
|
||||
|
||||
test('视图按依赖拓扑排序且同层按名称稳定排序', () => {
|
||||
const catalog = {
|
||||
tables: ['market_snapshot'],
|
||||
views: ['z_dashboard', 'market_summary', 'a_dashboard'],
|
||||
viewDependencies: [
|
||||
{
|
||||
view: 'z_dashboard',
|
||||
referencedSchema: 'a_share_analysis_dev',
|
||||
referencedObject: 'market_summary',
|
||||
},
|
||||
{
|
||||
view: 'a_dashboard',
|
||||
referencedSchema: 'a_share_analysis_dev',
|
||||
referencedObject: 'market_summary',
|
||||
},
|
||||
{
|
||||
view: 'market_summary',
|
||||
referencedSchema: 'a_share_analysis_dev',
|
||||
referencedObject: 'market_snapshot',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.deepEqual(orderViewNames(catalog), ['market_summary', 'a_dashboard', 'z_dashboard']);
|
||||
});
|
||||
|
||||
test('视图排序拒绝循环、跨库和未知依赖', () => {
|
||||
const base = { tables: ['market_snapshot'], views: ['first_view', 'second_view'] };
|
||||
const invalidCases = [
|
||||
{
|
||||
code: 'VIEW_DEPENDENCY_CYCLE',
|
||||
dependencies: [
|
||||
{ view: 'first_view', referencedSchema: 'a_share_analysis_dev', referencedObject: 'second_view' },
|
||||
{ view: 'second_view', referencedSchema: 'a_share_analysis_dev', referencedObject: 'first_view' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'CROSS_SCHEMA_VIEW_DEPENDENCY',
|
||||
dependencies: [
|
||||
{ view: 'first_view', referencedSchema: 'production', referencedObject: 'market_snapshot' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'UNKNOWN_VIEW_DEPENDENCY',
|
||||
dependencies: [
|
||||
{ view: 'first_view', referencedSchema: 'a_share_analysis_dev', referencedObject: 'missing_object' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const invalid of invalidCases) {
|
||||
assert.throws(
|
||||
() => orderViewNames({ ...base, viewDependencies: invalid.dependencies }),
|
||||
(error) => error instanceof SchemaExportError && error.code === invalid.code,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('目录拒绝未知表类型和超过对象上限', async () => {
|
||||
const unknownType = createCatalogConnection({
|
||||
'information_schema.TABLES': [{ TABLE_NAME: 'temporary_data', TABLE_TYPE: 'SYSTEM VIEW' }],
|
||||
});
|
||||
await assert.rejects(
|
||||
() => readSchemaCatalog(unknownType, 'a_share_analysis_dev', DEFAULT_EXPORT_LIMITS),
|
||||
(error) => error instanceof SchemaExportError && error.code === 'UNKNOWN_TABLE_TYPE',
|
||||
);
|
||||
|
||||
const overLimit = createCatalogConnection({
|
||||
'information_schema.TABLES': [
|
||||
{ TABLE_NAME: 'first_table', TABLE_TYPE: 'BASE TABLE' },
|
||||
{ TABLE_NAME: 'second_table', TABLE_TYPE: 'BASE TABLE' },
|
||||
],
|
||||
});
|
||||
await assert.rejects(
|
||||
() => readSchemaCatalog(overLimit, 'a_share_analysis_dev', { ...DEFAULT_EXPORT_LIMITS, maxObjects: 1 }),
|
||||
(error) => error instanceof SchemaExportError && error.code === 'OBJECT_LIMIT_EXCEEDED',
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user