diff --git a/AGENTS.md b/AGENTS.md index 74e0458..47e3edf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **a-share-analysis** (2373 symbols, 3222 relationships, 63 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **a-share-analysis** (2382 symbols, 3233 relationships, 63 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/database/schema-baselines/v0.0.9/manifest.yaml b/database/schema-baselines/v0.0.9/manifest.yaml new file mode 100644 index 0000000..fc56c99 --- /dev/null +++ b/database/schema-baselines/v0.0.9/manifest.yaml @@ -0,0 +1,33 @@ +{ + "format_version": 3, + "status": "captured_empty", + "application_version": "v0.0.9", + "git_commit": "88e2fdffc8c71dd682405c148a81953a8dcee9d6", + "source_environment": "development", + "database_name": "a_share_analysis_dev", + "charset": "utf8mb4", + "collation": "utf8mb4_general_ci", + "generated_at": "2026-08-17T18:56:33.473Z", + "generator": "schema-baseline/3.0.0", + "reason": "none", + "schema_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "objects": { + "tables": false, + "views": false, + "triggers": false, + "routines": false, + "events": false + }, + "object_counts": { + "tables": 0, + "views": 0, + "triggers": 0, + "routines": 0, + "events": 0 + }, + "exporter_name": "mysql2-show-create", + "exporter_version": "1.0.0", + "database_driver": "mysql2", + "database_driver_version": "3.23.3", + "mysql_server_version": "5.7.42-0ubuntu0.18.04.1" +} diff --git a/tools/database/mysql2-schema-exporter.mjs b/tools/database/mysql2-schema-exporter.mjs index f84535b..eda6943 100644 --- a/tools/database/mysql2-schema-exporter.mjs +++ b/tools/database/mysql2-schema-exporter.mjs @@ -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) { diff --git a/tools/database/mysql2-schema-exporter.test.mjs b/tools/database/mysql2-schema-exporter.test.mjs index d848fdb..871b98f 100644 --- a/tools/database/mysql2-schema-exporter.test.mjs +++ b/tools/database/mysql2-schema-exporter.test.mjs @@ -153,6 +153,49 @@ test('目录查询使用固定参数和超时并返回稳定对象', async () => } }); +test('MySQL 5.7 缺少 VIEW_TABLE_USAGE 时从 VIEWS 定义解析依赖', async () => { + const fallback = createCatalogConnection(); + const connection = { + calls: fallback.calls, + async query(options) { + if (options.sql.includes('information_schema.VIEW_TABLE_USAGE')) { + throw Object.assign(new Error('unknown metadata table'), { code: 'ER_UNKNOWN_TABLE' }); + } + if (options.sql.includes('information_schema.VIEWS')) { + return [ + [ + { + VIEW_NAME: 'market_dashboard', + VIEW_DEFINITION: + 'SELECT * FROM `a_share_analysis_dev`.`market_summary`', + }, + { + VIEW_NAME: 'market_summary', + VIEW_DEFINITION: 'SELECT * FROM `market_snapshot`', + }, + ], + [], + ]; + } + return fallback.query(options); + }, + }; + + const catalog = await readSchemaCatalog(connection, 'a_share_analysis_dev', DEFAULT_EXPORT_LIMITS); + assert.deepEqual(catalog.viewDependencies, [ + { + view: 'market_dashboard', + referencedSchema: 'a_share_analysis_dev', + referencedObject: 'market_summary', + }, + { + view: 'market_summary', + referencedSchema: 'a_share_analysis_dev', + referencedObject: 'market_snapshot', + }, + ]); +}); + test('视图按依赖拓扑排序且同层按名称稳定排序', () => { const catalog = { tables: ['market_snapshot'],