From a920c4bbbf29dff5fffe4f9a347548ef6364eedf Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:05:38 -0500 Subject: [PATCH] fix(@angular/cli): add schema versioning and metadata to example database This commit introduces schema versioning and metadata to the Angular CLI's example database. A new `metadata` table has been added to both the runtime and build-time example databases. This table stores key-value pairs, initially including `schema_version` and `created_at`. The `find_examples` tool now validates the `schema_version` of the loaded database. If a schema mismatch is detected, the tool throws a descriptive error message that guides the user on how to resolve the incompatibility (e.g., by updating their CLI or project's `@angular/core` package). This change improves the robustness and maintainability of the example database by: - Ensuring compatibility between the CLI and the example database schema. - Providing actionable feedback to users when schema versions are incompatible. - Laying the groundwork for future metadata additions and database evolution. --- .../cli/src/commands/mcp/tools/examples.ts | 61 ++++++++++++++++--- tools/example_db_generator.js | 14 +++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/examples.ts b/packages/angular/cli/src/commands/mcp/tools/examples.ts index 709bbebfc6ae..97bc71a94e8a 100644 --- a/packages/angular/cli/src/commands/mcp/tools/examples.ts +++ b/packages/angular/cli/src/commands/mcp/tools/examples.ts @@ -20,8 +20,8 @@ const findExampleInputSchema = z.object({ .describe( 'The absolute path to the `angular.json` file for the workspace. This is used to find the ' + 'version-specific code examples that correspond to the installed version of the ' + - 'Angular framework. You **MUST** get this path from the `list_projects` tool. If omitted, ' + - 'the tool will search the generic code examples bundled with the CLI.', + 'Angular framework. You **MUST** get this path from the `list_projects` tool. ' + + 'If omitted, the tool will search the generic code examples bundled with the CLI.', ), query: z .string() @@ -306,28 +306,60 @@ async function createFindExampleHandler({ logger, exampleDatabasePath }: McpTool return queryDatabase(runtimeDb, input); } - let dbPath: string | undefined; + let resolvedDbPath: string | undefined; + let dbSource: string | undefined; // First, try to get the version-specific guide. if (input.workspacePath) { const versionSpecific = await getVersionSpecificExampleDatabase(input.workspacePath, logger); if (versionSpecific) { - dbPath = versionSpecific.dbPath; + resolvedDbPath = versionSpecific.dbPath; + dbSource = versionSpecific.source; } } // If the version-specific guide was not found for any reason, fall back to the bundled version. - if (!dbPath) { - dbPath = exampleDatabasePath; + if (!resolvedDbPath) { + resolvedDbPath = exampleDatabasePath; + dbSource = 'bundled'; } - if (!dbPath) { + if (!resolvedDbPath) { // This should be prevented by the registration logic in mcp-server.ts throw new Error('Example database path is not available.'); } const { DatabaseSync } = await import('node:sqlite'); - const db = new DatabaseSync(dbPath, { readOnly: true }); + const db = new DatabaseSync(resolvedDbPath, { readOnly: true }); + + // Validate the schema version of the database. + const EXPECTED_SCHEMA_VERSION = 1; + const schemaVersionResult = db + .prepare('SELECT value FROM metadata WHERE key = ?') + .get('schema_version') as { value: string } | undefined; + const actualSchemaVersion = schemaVersionResult ? Number(schemaVersionResult.value) : undefined; + + if (actualSchemaVersion !== EXPECTED_SCHEMA_VERSION) { + db.close(); + + let errorMessage: string; + if (actualSchemaVersion === undefined) { + errorMessage = 'The example database is missing a schema version and cannot be used.'; + } else if (actualSchemaVersion > EXPECTED_SCHEMA_VERSION) { + errorMessage = + `This project's example database (version ${actualSchemaVersion})` + + ` is newer than what this version of the Angular CLI supports (version ${EXPECTED_SCHEMA_VERSION}).` + + ' Please update your `@angular/cli` package to a newer version.'; + } else { + errorMessage = + `This version of the Angular CLI (expects schema version ${EXPECTED_SCHEMA_VERSION})` + + ` requires a newer example database than the one found in this project (version ${actualSchemaVersion}).`; + } + + throw new Error( + `Incompatible example database schema from source '${dbSource}':\n${errorMessage}`, + ); + } return queryDatabase(db, input); }; @@ -571,6 +603,19 @@ async function setupRuntimeExamples(examplesPath: string): Promise const db = new DatabaseSync(':memory:'); // Create a relational table to store the structured example data. + db.exec(` + CREATE TABLE metadata ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT NOT NULL + ); + `); + + db.exec(` + INSERT INTO metadata (key, value) VALUES + ('schema_version', '1'), + ('created_at', '${new Date().toISOString()}'); + `); + db.exec(` CREATE TABLE examples ( id INTEGER PRIMARY KEY, diff --git a/tools/example_db_generator.js b/tools/example_db_generator.js index 142bd1e8a7ed..052bb1afb53d 100644 --- a/tools/example_db_generator.js +++ b/tools/example_db_generator.js @@ -89,6 +89,20 @@ function generate(inPath, outPath) { } const db = new DatabaseSync(dbPath); + // Create a table to store metadata. + db.exec(` + CREATE TABLE metadata ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT NOT NULL + ); + `); + + db.exec(` + INSERT INTO metadata (key, value) VALUES + ('schema_version', '1'), + ('created_at', '${new Date().toISOString()}'); + `); + // Create a relational table to store the structured example data. db.exec(` CREATE TABLE examples (