From 20e5e76f24ab8474c2c901e46ccdfab65d84d441 Mon Sep 17 00:00:00 2001 From: Vadim Kharitonov Date: Wed, 8 Oct 2025 12:12:23 +0200 Subject: [PATCH 1/6] Adding a schema diff tool that will automatically suggest to generate a zero downtime migration --- src/tools/definitions.ts | 63 ++++++++++++++++++++++++++++++++++++++++ src/tools/tools.ts | 23 +++++++++++++++ src/tools/toolsSchema.ts | 6 ++++ 3 files changed, 92 insertions(+) diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index f5e6050..069b5ec 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -23,6 +23,7 @@ import { listOrganizationsInputSchema, listSharedProjectsInputSchema, resetFromParentInputSchema, + compareDatabaseSchemaInputSchema, } from './toolsSchema.js'; export const NEON_TOOLS = [ @@ -608,4 +609,66 @@ export const NEON_TOOLS = [ description: 'Lists compute endpoints for a project or specific branch', inputSchema: listBranchComputesInputSchema, }, + { + name: 'compare_database_schema' as const, + description: ` + + Use this tool to compare the schema of a database between two branches. + The output of the tool is a JSON object with one field: \`diff\`. + At this field you will find a difference between two schemas. + + You MUST BE READY to generate a zero-downtime migration from the diff and apply it to the parent branch. + + + + To generate schema diff, you MUST SPECIFY the \`database_name\`. + If it's not specified, try to use the default database name: \`${NEON_DEFAULT_DATABASE_NAME}\`. + + You MUST TAKE INTO ACCOUNT the Postgres version. The Postgres version is the same for both branches. + You MUST ASK user consent before running each generated SQL query. + You SHOULD USE \`run_sql\` tool to run each generated SQL query. + Generated queries change the schema of the parent branch and MIGHT BE dangerous to execute. + + + + After executing this tool, you MUST follow these steps: + 1. Review the schema diff and suggest to generate a zero-downtime migration. + 2. Follow these instructions to respond to the client: + + + + Provide a brief information about the changes: + * Tables + * Views + * Indexes + * Ownership + * Constraints + * Triggers + * Policies + * Extensions + * Schemas + * Sequences + * Tablespaces + * Users + * Roles + * Privileges + + + + + This tool: + 1. Generates a diff between two branches. + 2. Generates a SQL migration from the diff. + 3. Suggest to generate zero-downtime migration. + + Workflow: + 1. User asks you to generate a diff between two branches. + 2. You suggest to generate a SQL migration from the diff. + 3. You ensure that the generated migration is a zero-downtime migration, + otherwise you should warn the user about it. + 4. You ensure that your suggested migration is also matching the Postgres version. + 5. You use \`run_sql\` tool to run each generated SQL query and ask the user consent before running it. + `, + inputSchema: compareDatabaseSchemaInputSchema, + }, ]; diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 864b441..5011eb9 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -4,6 +4,7 @@ import { EndpointType, ListProjectsParams, ListSharedProjectsParams, + GetProjectBranchSchemaComparisonParams, Organization, ProjectCreateRequest, } from '@neondatabase/api-client'; @@ -1212,6 +1213,14 @@ async function handleListSharedProjects( return response.data.projects; } +async function handleCompareDatabaseSchema( + params: GetProjectBranchSchemaComparisonParams, + neonClient: Api, +) { + const response = await neonClient.getProjectBranchSchemaComparison(params); + return response.data; +} + export const NEON_HANDLERS = { list_projects: async ({ params }, neonClient, extra) => { const organization = await getOrgByOrgIdOrDefault( @@ -1774,4 +1783,18 @@ export const NEON_HANDLERS = { ], }; }, + + compare_database_schema: async ({ params }, neonClient) => { + const result = await handleCompareDatabaseSchema( + { + projectId: params.projectId, + branchId: params.branchId, + db_name: params.databaseName, + }, + neonClient + ); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + }, } satisfies ToolHandlers; diff --git a/src/tools/toolsSchema.ts b/src/tools/toolsSchema.ts index 855aaf0..226fbdb 100644 --- a/src/tools/toolsSchema.ts +++ b/src/tools/toolsSchema.ts @@ -334,3 +334,9 @@ export const resetFromParentInputSchema = z.object({ 'Optional name to preserve the current state under a new branch before resetting', ), }); + +export const compareDatabaseSchemaInputSchema = z.object({ + projectId: z.string().describe('The ID of the project'), + branchId: z.string().describe('The ID of the branch'), + databaseName: z.string().describe(DATABASE_NAME_DESCRIPTION), +}); \ No newline at end of file From 71445dc90d4591bb98f4e85b109a74d9d686b29d Mon Sep 17 00:00:00 2001 From: Vadim Kharitonov Date: Wed, 8 Oct 2025 12:20:07 +0200 Subject: [PATCH 2/6] Fix lint --- src/tools/tools.ts | 2 +- src/tools/toolsSchema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 5011eb9..5f3a8d5 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -1791,7 +1791,7 @@ export const NEON_HANDLERS = { branchId: params.branchId, db_name: params.databaseName, }, - neonClient + neonClient, ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], diff --git a/src/tools/toolsSchema.ts b/src/tools/toolsSchema.ts index 226fbdb..95e14ba 100644 --- a/src/tools/toolsSchema.ts +++ b/src/tools/toolsSchema.ts @@ -339,4 +339,4 @@ export const compareDatabaseSchemaInputSchema = z.object({ projectId: z.string().describe('The ID of the project'), branchId: z.string().describe('The ID of the branch'), databaseName: z.string().describe(DATABASE_NAME_DESCRIPTION), -}); \ No newline at end of file +}); From 9bfc73ebe08f93e1ec93902668e1abe6ba3b6b74 Mon Sep 17 00:00:00 2001 From: Vadim Kharitonov Date: Wed, 8 Oct 2025 14:54:02 +0200 Subject: [PATCH 3/6] Updated prompt --- src/tools/definitions.ts | 168 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 12 deletions(-) diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index 069b5ec..d7f58f8 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -615,29 +615,45 @@ export const NEON_TOOLS = [ Use this tool to compare the schema of a database between two branches. The output of the tool is a JSON object with one field: \`diff\`. + + + \`\`\`json + { + "diff": "--- a/neondb\n+++ b/neondb\n@@ -27,7 +27,10 @@\n \n CREATE TABLE public.users (\n id integer NOT NULL,\n- username character varying(50) NOT NULL\n+ username character varying(50) NOT NULL,\n+ is_deleted boolean DEFAULT false NOT NULL,\n+ created_at timestamp with time zone DEFAULT now() NOT NULL,\n+ updated_at timestamp with time zone\n );\n \n \n@@ -79,6 +82,13 @@\n \n \n --\n+-- Name: users_created_at_idx; Type: INDEX; Schema: public; Owner: neondb_owner\n+--\n+\n+CREATE INDEX users_created_at_idx ON public.users USING btree (created_at DESC) WHERE (is_deleted = false);\n+\n+\n+--\n -- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: public; Owner: cloud_admin\n --\n \n" + } + \`\`\` + + At this field you will find a difference between two schemas. + The diff represents the changes required to make the parent branch schema match the child branch schema. + The diff field contains a unified diff (git-style patch) as a string. - You MUST BE READY to generate a zero-downtime migration from the diff and apply it to the parent branch. + You MUST be able to generate a zero-downtime migration from the diff and apply it to the parent branch. + (This branch is a child and has a parent. You can get parent id just querying the branch details.) To generate schema diff, you MUST SPECIFY the \`database_name\`. - If it's not specified, try to use the default database name: \`${NEON_DEFAULT_DATABASE_NAME}\`. + If \`database_name\` is not specified, you MUST fall back to the default database name: \`${NEON_DEFAULT_DATABASE_NAME}\`. - You MUST TAKE INTO ACCOUNT the Postgres version. The Postgres version is the same for both branches. + You MUST TAKE INTO ACCOUNT the PostgreSQL version. The PostgreSQL version is the same for both branches. You MUST ASK user consent before running each generated SQL query. You SHOULD USE \`run_sql\` tool to run each generated SQL query. + You SHOULD suggest creating a backup or point-in-time restore before running the migration. Generated queries change the schema of the parent branch and MIGHT BE dangerous to execute. + Generated SQL migrations SHOULD be idempotent where possible (i.e., safe to run multiple times without failure) and include \`IF NOT EXISTS\` / \`IF EXISTS\` where applicable. + You SHOULD recommend including comments in generated SQL linking back to diff hunks (e.g., \`-- from diff @@ -27,7 +27,10 @@\`) to make audits easier. + Generated SQL should be reviewed for dependencies (e.g., foreign key order) before execution. After executing this tool, you MUST follow these steps: - 1. Review the schema diff and suggest to generate a zero-downtime migration. + 1. Review the schema diff and suggest generating a zero-downtime migration. 2. Follow these instructions to respond to the client: - Provide a brief information about the changes: + Provide brief information about the changes: * Tables * Views * Indexes @@ -654,20 +670,148 @@ export const NEON_TOOLS = [ * Privileges + + 3. If a migration fails, you SHOULD guide the user on how to revert the schema changes, for example by using backups, point-in-time restore, or generating reverse SQL statements (if safe). This tool: - 1. Generates a diff between two branches. + 1. Generates a diff between the child branch and its parent. 2. Generates a SQL migration from the diff. - 3. Suggest to generate zero-downtime migration. + 3. Suggest generating zero-downtime migration. - Workflow: + 1. User asks you to generate a diff between two branches. - 2. You suggest to generate a SQL migration from the diff. - 3. You ensure that the generated migration is a zero-downtime migration, - otherwise you should warn the user about it. - 4. You ensure that your suggested migration is also matching the Postgres version. + 2. You suggest generating a SQL migration from the diff. + 3. Ensure the generated migration is zero-downtime; otherwise, warn the user. + 4. You ensure that your suggested migration is also matching the PostgreSQL version. 5. You use \`run_sql\` tool to run each generated SQL query and ask the user consent before running it. + Before requesting user consent, present a summary of all generated SQL statements along with their potential impact (e.g., table rewrites, lock risks, validation steps) so the user can make an informed decision. + 6. Propose to rerun the schema diff tool one more time to ensure that the migration is applied correctly. + 7. If the diff is empty, confirm that the parent schema now matches the child schema. + 8. If the diff is not empty after migration, warn the user and assist in resolving the remaining differences. + + + + + Adding the column with a \`DEFAULT\` static value will not have any locks. + But if the function is called that is not deterministic, it will have locks. + + + \`\`\`sql + -- No table rewrite, minimal lock time + ALTER TABLE users ADD COLUMN status text DEFAULT 'active'; + \`\`\` + + + + \`\`\`sql + -- Table rewrite, potentially longer lock time + ALTER TABLE users ADD COLUMN created_at timestamptz DEFAULT now(); + \`\`\` + + The fix for this is next: + + \`\`\`sql + -- Table rewrite, potentially longer lock time + ALTER TABLE users ADD COLUMN created_at timestamptz; + + UPDATE users SET created_at = now(); + \`\`\` + + + + + Adding constraints in two phases (including foreign keys) + + + \`\`\`sql + -- Step 1: Add constraint without validating existing data + -- Fast - only blocks briefly to update catalog + ALTER TABLE users ADD CONSTRAINT users_age_positive + CHECK (age > 0) NOT VALID; + + -- Step 2: Validate existing data (can take time but doesn't block writes) + -- Uses SHARE UPDATE EXCLUSIVE lock - allows reads/writes + ALTER TABLE users VALIDATE CONSTRAINT users_age_positive; + \`\`\` + + + + \`\`\`sql + -- Step 1: Add foreign key without validation + -- Fast - only updates catalog, doesn't validate existing data + ALTER TABLE orders ADD CONSTRAINT orders_user_id_fk + FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID; + + -- Step 2: Validate existing relationships + -- Can take time but allows concurrent operations + ALTER TABLE orders VALIDATE CONSTRAINT orders_user_id_fk; + \`\`\` + + + + + Setting columns to NOT NULL + + + \`\`\`sql + -- Step 1: Add a check constraint (fast with NOT VALID) + ALTER TABLE users ADD CONSTRAINT users_email_not_null + CHECK (email IS NOT NULL) NOT VALID; + + -- Step 2: Validate the constraint (allows concurrent operations) + ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null; + + -- Step 3: Set NOT NULL (fast since constraint guarantees no nulls) + ALTER TABLE users ALTER COLUMN email SET NOT NULL; + + -- Step 4: Drop the redundant check constraint + ALTER TABLE users DROP CONSTRAINT users_email_not_null; + \`\`\` + + + + For PostgreSQL v18+ + (to get PostgreSQL version, you can use \`describe_project\` tool or \`run_sql\` tool and execute \`SELECT version();\` query) + + \`\`\`sql + -- PostgreSQL 18+ - Simplified approach + ALTER TABLE users ALTER COLUMN email SET NOT NULL NOT VALID; + ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null; + \`\`\` + + + + + Create index CONCURRENTLY + + + \`\`\`sql + CREATE INDEX CONCURRENTLY idx_users_email ON users (email); + \`\`\` + + + + + Drop index CONCURRENTLY + + + \`\`\`sql + DROP INDEX CONCURRENTLY idx_users_email; + \`\`\` + + + + + Refresh materialized view CONCURRENTLY + + + \`\`\`sql + REFRESH MATERIALIZED VIEW CONCURRENTLY mv_users; + \`\`\` + + + `, inputSchema: compareDatabaseSchemaInputSchema, }, From 864adcc137696e5e48dad421fb86f5d6c957fccc Mon Sep 17 00:00:00 2001 From: Vadim Kharitonov Date: Wed, 8 Oct 2025 15:21:48 +0200 Subject: [PATCH 4/6] one more hint is added --- src/tools/definitions.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index d7f58f8..a65daa1 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -782,6 +782,58 @@ export const NEON_TOOLS = [ + + In some cases, you need to combine two approaches to achieve a zero-downtime migration. + + + \`\`\`sql + -- Step 1: Adding a nullable column first + ALTER TABLE users ADD COLUMN created_at timestamptz; + + -- Step 2: Updating the all rows with the default value + UPDATE users SET created_at = now() WHERE created_at IS NULL; + + -- Step 3: Creating a not null constraint + ALTER TABLE users ADD CONSTRAINT users_created_at_not_null + CHECK (created_at IS NOT NULL) NOT VALID; + + -- Step 4: Validating the constraint + ALTER TABLE users VALIDATE CONSTRAINT users_created_at_not_null; + + -- Step 5: Setting the column to NOT NULL + ALTER TABLE users ALTER COLUMN created_at SET NOT NULL; + + -- Step 6: Dropping the redundant NOT NULL constraint + ALTER TABLE users DROP CONSTRAINT users_created_at_not_null; + + -- Step 7: Adding the default value + ALTER TABLE users ALTER COLUMN created_at SET DEFAULT now(); + \`\`\` + + + For PostgreSQL v18+ + + + \`\`\`sql + -- Step 1: Adding a nullable column first + ALTER TABLE users ADD COLUMN created_at timestamptz; + + -- Step 2: Updating the all rows with the default value + UPDATE users SET created_at = now() WHERE created_at IS NULL; + + -- Step 3: Creating a not null constraint + ALTER TABLE users ALTER COLUMN created_at SET NOT NULL NOT VALID; + + -- Step 4: Validating the constraint + ALTER TABLE users VALIDATE CONSTRAINT users_created_at_not_null; + + -- Step 5: Adding the default value + ALTER TABLE users ALTER COLUMN created_at SET DEFAULT now(); + \`\`\` + + + + Create index CONCURRENTLY From 541df2fcf3fabf6927970e99f293bfa7a8b41dde Mon Sep 17 00:00:00 2001 From: Vadim Kharitonov Date: Wed, 8 Oct 2025 22:49:18 +0200 Subject: [PATCH 5/6] Added hints support to the landing + some minor fixes --- CHANGELOG.md | 1 + README.md | 1 + landing/components/DescriptionItem.tsx | 4 ++++ landing/lib/description.ts | 4 +++- src/tools/definitions.ts | 13 +++++++------ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f4e73..d0ad3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Feat: `list_shared_projects` tool to fetch projects that user has permissions to collaborate on - Feat: `reset_from_parent` tool to reset a branch from its parent's current state +- Feat: `compare_database_schema` tool to compare the schema from the child branch and its parent # [0.6.4] 2025-08-22 diff --git a/README.md b/README.md index a1e89c6..2b26d06 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ The Neon MCP Server provides the following actions, which are exposed as "tools" - **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, including compute ID, type, size, and autoscaling information. - **`list_organizations`**: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter. - **`reset_from_parent`**: Resets the current branch to its parent's state, discarding local changes. Automatically preserves to backup if branch has children, or optionally preserve on request with a custom name. +- **`compare_database_schema`**: Shows the schema diff between the child branch and its parent **SQL Query Execution:** diff --git a/landing/components/DescriptionItem.tsx b/landing/components/DescriptionItem.tsx index 0eeaf35..ca65c8e 100644 --- a/landing/components/DescriptionItem.tsx +++ b/landing/components/DescriptionItem.tsx @@ -13,6 +13,8 @@ import { import { Terminal, CircleAlert, + Lightbulb, + BadgeInfo, Workflow, SquareArrowRight, Component, @@ -30,6 +32,8 @@ const ALERT_VARIANT_PER_DESCRIPTION_TYPE: Record< next_steps: { variant: 'default', icon: SquareArrowRight }, important_notes: { variant: 'important', icon: CircleAlert }, workflow: { variant: 'default', icon: Workflow }, + hints: { variant: 'default', icon: BadgeInfo }, + hint: { variant: 'default', icon: Lightbulb }, instructions: { variant: 'default', icon: Terminal }, response_instructions: { variant: 'default', icon: Terminal }, example: { variant: 'default', icon: Terminal }, diff --git a/landing/lib/description.ts b/landing/lib/description.ts index b7125ed..d3c7f11 100644 --- a/landing/lib/description.ts +++ b/landing/lib/description.ts @@ -10,6 +10,8 @@ const POSSIBLE_TYPES = [ 'example', 'do_not_include', 'error_handling', + 'hint', + 'hints', ] as const; export type DescriptionItemType = (typeof POSSIBLE_TYPES)[number]; @@ -134,7 +136,7 @@ export function parseDescription(description: string): DescriptionItem[] { while (rest.length > 0) { const match = rest.match( - /<(use_case|workflow|important_notes|next_steps|response_instructions|instructions|example|do_not_include|error_handling)>(.*?)<\/\1>/s, + /<(use_case|workflow|important_notes|next_steps|response_instructions|instructions|example|do_not_include|error_handling|hints?)>(.*?)<\/\1>/s, ); if (!match) { diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index a65daa1..8c969f7 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -703,6 +703,8 @@ export const NEON_TOOLS = [ \`\`\` + There is an example of a case where the function is not deterministic and will have locks: + \`\`\`sql -- Table rewrite, potentially longer lock time @@ -712,9 +714,10 @@ export const NEON_TOOLS = [ The fix for this is next: \`\`\`sql - -- Table rewrite, potentially longer lock time + -- Adding a nullable column first ALTER TABLE users ADD COLUMN created_at timestamptz; + -- Setting the default value because the rows are updated UPDATE users SET created_at = now(); \`\`\` @@ -730,9 +733,9 @@ export const NEON_TOOLS = [ ALTER TABLE users ADD CONSTRAINT users_age_positive CHECK (age > 0) NOT VALID; - -- Step 2: Validate existing data (can take time but doesn't block writes) - -- Uses SHARE UPDATE EXCLUSIVE lock - allows reads/writes - ALTER TABLE users VALIDATE CONSTRAINT users_age_positive; + -- Step 2: Validate existing data (can take time but doesn't block writes) + -- Uses SHARE UPDATE EXCLUSIVE lock - allows reads/writes + ALTER TABLE users VALIDATE CONSTRAINT users_age_positive; \`\`\` @@ -812,7 +815,6 @@ export const NEON_TOOLS = [ For PostgreSQL v18+ - \`\`\`sql -- Step 1: Adding a nullable column first @@ -831,7 +833,6 @@ export const NEON_TOOLS = [ ALTER TABLE users ALTER COLUMN created_at SET DEFAULT now(); \`\`\` - From 84e35eecc679b37ca8f7c279e06e0155dd650d03 Mon Sep 17 00:00:00 2001 From: Vadim Kharitonov Date: Wed, 8 Oct 2025 23:07:58 +0200 Subject: [PATCH 6/6] hint for materialized view --- src/tools/definitions.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index 8c969f7..914041c 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -855,6 +855,16 @@ export const NEON_TOOLS = [ + + Create materialized view WITH NO DATA + + + \`\`\`sql + CREATE MATERIALIZED VIEW mv_users AS SELECT name FROM users WITH NO DATA; + \`\`\` + + + Refresh materialized view CONCURRENTLY