From 30cc8dfa89d8c9770f41385958aa2efbd1596ff7 Mon Sep 17 00:00:00 2001 From: Jasdeep Date: Thu, 15 Oct 2026 23:09:26 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20DBDiff=20rc.29=20=E2=80=94=20renames,?= =?UTF-8?q?=20more=20object=20kinds,=20and=20what=20it=20does=20not=20comp?= =?UTF-8?q?are?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @dbdiff/cli 3.0.0-rc.29 renames a column, table or sequence, keeping its rows, instead of dropping and creating it again; compares range types, aggregates, default privileges, event triggers, rules, casts, extended statistics, text search, foreign data, replica identity and statistics targets; and names the kinds it does not compare, which SupaForge shows beside the schema check. e2e: renames keep their rows, the new kinds sync and converge, and an operator is named without being counted, through the CLI, read from the target. Co-Authored-By: Claude Opus 5.5 --- packages/cli/package-lock.json | 76 ++++++------- packages/cli/package.json | 2 +- .../cli/test/e2e/renames-and-kinds.test.ts | 106 ++++++++++++++++++ 3 files changed, 145 insertions(+), 39 deletions(-) create mode 100644 packages/cli/test/e2e/renames-and-kinds.test.ts diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index f2d3d7e..a793326 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,16 +1,16 @@ { "name": "@akal/supaforge", - "version": "0.0.33", + "version": "0.0.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@akal/supaforge", - "version": "0.0.33", + "version": "0.0.34", "license": "MIT", "dependencies": { "@akal/pg-conformance": "~0.0.16", - "@dbdiff/cli": "3.0.0-rc.28", + "@dbdiff/cli": "3.0.0-rc.29", "@modelcontextprotocol/sdk": "^1.32.1", "@oclif/core": "^4", "@supabase/supabase-js": "^2.103.0", @@ -43,9 +43,9 @@ } }, "node_modules/@dbdiff/cli": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli/-/cli-3.0.0-rc.28.tgz", - "integrity": "sha512-40KbeSemRZpsnz8Kwt9yywjGClb9xlftPC0nGLExttRQ/FzwskAUzpipUfxHVK4NhxPV8CHugUwC+Se35sB/QQ==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli/-/cli-3.0.0-rc.29.tgz", + "integrity": "sha512-brwSBOJCRwlsHaDF1ECuHQ0m4y0jlGyZFWmjvU4SMYhCAdl8ascr9Qy/ZjYcGhch9gjPqqmNpIjWISWdRrHbsw==", "license": "MIT", "bin": { "dbdiff": "bin/dbdiff.js" @@ -54,20 +54,20 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@dbdiff/cli-darwin-arm64": "3.0.0-rc.28", - "@dbdiff/cli-darwin-x64": "3.0.0-rc.28", - "@dbdiff/cli-linux-arm64": "3.0.0-rc.28", - "@dbdiff/cli-linux-arm64-musl": "3.0.0-rc.28", - "@dbdiff/cli-linux-x64": "3.0.0-rc.28", - "@dbdiff/cli-linux-x64-musl": "3.0.0-rc.28", - "@dbdiff/cli-win32-arm64": "3.0.0-rc.28", - "@dbdiff/cli-win32-x64": "3.0.0-rc.28" + "@dbdiff/cli-darwin-arm64": "3.0.0-rc.29", + "@dbdiff/cli-darwin-x64": "3.0.0-rc.29", + "@dbdiff/cli-linux-arm64": "3.0.0-rc.29", + "@dbdiff/cli-linux-arm64-musl": "3.0.0-rc.29", + "@dbdiff/cli-linux-x64": "3.0.0-rc.29", + "@dbdiff/cli-linux-x64-musl": "3.0.0-rc.29", + "@dbdiff/cli-win32-arm64": "3.0.0-rc.29", + "@dbdiff/cli-win32-x64": "3.0.0-rc.29" } }, "node_modules/@dbdiff/cli-darwin-arm64": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-darwin-arm64/-/cli-darwin-arm64-3.0.0-rc.28.tgz", - "integrity": "sha512-1nAfbgD6ThjZpHl2loUNq8zUiA1/cY88/kzWs4vy/VXAtlkaKF0l5KPttV6MkPOdUyzlqoMQQY0zhWvDku5SzA==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-darwin-arm64/-/cli-darwin-arm64-3.0.0-rc.29.tgz", + "integrity": "sha512-UY9/sPo+HWWmpX/uNr1XoDpQo+MM1Dc7qDxRVLE5/A8/qux21zqJ8KBjqHa5dvPaV3WoZ6ttusYR/qc+noS05g==", "cpu": [ "arm64" ], @@ -78,9 +78,9 @@ ] }, "node_modules/@dbdiff/cli-darwin-x64": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-darwin-x64/-/cli-darwin-x64-3.0.0-rc.28.tgz", - "integrity": "sha512-4JEAWZe2cl54sFuvWs+THqoB5ZsU7VfbdajBx0tBirSlf7f9HVuhX0p5HfmkFhUcSIT4F7s3QxCxuGfQZOOYTQ==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-darwin-x64/-/cli-darwin-x64-3.0.0-rc.29.tgz", + "integrity": "sha512-8gqS2oOJUzp5kKJFxXcgFqCmO8lyisPPFvu/B4FQRhC8h4xtcchpH/FtutcPmLuPbj/JihWvlgoW+fIzZEDadA==", "cpu": [ "x64" ], @@ -91,9 +91,9 @@ ] }, "node_modules/@dbdiff/cli-linux-arm64": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-arm64/-/cli-linux-arm64-3.0.0-rc.28.tgz", - "integrity": "sha512-mMLNu/8M8Sj3mulNly0KbxVT5LLYLBvAvTBc6xmRPZfewVKMhdDcfsCctnxKN2utPSbHYi+HLC0SP5BT05Y5jg==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-arm64/-/cli-linux-arm64-3.0.0-rc.29.tgz", + "integrity": "sha512-DGqhm+K1QaYFA5Qd3kB1IejxnSqFwnQZUZ/3Jd9p+0FGGwiekhYDeBoFkvkL907s//MexVv17bJRUWg+xYupXw==", "cpu": [ "arm64" ], @@ -107,9 +107,9 @@ ] }, "node_modules/@dbdiff/cli-linux-arm64-musl": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-arm64-musl/-/cli-linux-arm64-musl-3.0.0-rc.28.tgz", - "integrity": "sha512-d2XJBK5tdE/dcClVpCupMeRtpt39TrSLP6W9H7GHES0bvlC3BBDp9/xUeAK/7XOQ6G4RifS0XLz34H4PYzfrbg==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-arm64-musl/-/cli-linux-arm64-musl-3.0.0-rc.29.tgz", + "integrity": "sha512-WunJz7zOljgYQxlTz8VHPBnIRbs+ndYXBZYsbbHtE6CxJSdzgz72zRP92vG1uBlXGpt8eG18/3cfnvS6wF5sxw==", "cpu": [ "arm64" ], @@ -123,9 +123,9 @@ ] }, "node_modules/@dbdiff/cli-linux-x64": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-x64/-/cli-linux-x64-3.0.0-rc.28.tgz", - "integrity": "sha512-wRe7byCYOXTN091otnqHxq0vQNkJ6Yg0TrsOHJB+6LTTUVAnfEJhzCJ0HDxfLY+DXWEXXU98ahrK9tFq9iy/2w==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-x64/-/cli-linux-x64-3.0.0-rc.29.tgz", + "integrity": "sha512-0ViMr4UpfL68teSGPevk9vTJlFMHBeMrmMZnszZkM457KSXuh6fnga9EtPO7DkCzwAMk24YeYod0KFjMZVK21w==", "cpu": [ "x64" ], @@ -139,9 +139,9 @@ ] }, "node_modules/@dbdiff/cli-linux-x64-musl": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-x64-musl/-/cli-linux-x64-musl-3.0.0-rc.28.tgz", - "integrity": "sha512-IyuC6GancKUtTSD68k89Y10lPnxecNo9TpClsF7Nknpkc7XCkGftzIjlePZ8OOpLq8w9Ln2zC8de+mMgdX/LsA==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-linux-x64-musl/-/cli-linux-x64-musl-3.0.0-rc.29.tgz", + "integrity": "sha512-i6AIwEEGOTGILjvsZYRygY715SLow3v5P5x6Twtq+VBoI+zpQo8H70JS+cK2Lua+mdu8m/P3zPAn+LkqxcJ8ig==", "cpu": [ "x64" ], @@ -155,9 +155,9 @@ ] }, "node_modules/@dbdiff/cli-win32-arm64": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-win32-arm64/-/cli-win32-arm64-3.0.0-rc.28.tgz", - "integrity": "sha512-dZhPffLekZGn0SyE3hPG4gspCwbOj30ZAL/fh0jBFuNHLOEJBzY4YQ/57jvqiUjlBnLgyKF1U85ADCwOf1GTxw==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-win32-arm64/-/cli-win32-arm64-3.0.0-rc.29.tgz", + "integrity": "sha512-ad4fcP8LZnzSHPSuYNU79jNQTyIKffUZforQc3bNOzbrt051IGxtAxRnXsvisqffa98wnbGFuPDa5eECJec8hw==", "cpu": [ "arm64" ], @@ -168,9 +168,9 @@ ] }, "node_modules/@dbdiff/cli-win32-x64": { - "version": "3.0.0-rc.28", - "resolved": "https://registry.npmjs.org/@dbdiff/cli-win32-x64/-/cli-win32-x64-3.0.0-rc.28.tgz", - "integrity": "sha512-JaPInZ07FBvEQ2V5QUKI+2K3ooTCy2hxf1GIVNTTtbFBQvrDZvDFBEYm+RmbI53G3lkjBdJlWTLt9+VIFd5oMA==", + "version": "3.0.0-rc.29", + "resolved": "https://registry.npmjs.org/@dbdiff/cli-win32-x64/-/cli-win32-x64-3.0.0-rc.29.tgz", + "integrity": "sha512-zASma8uXzDYMjr6rwYmEr/so4HoHx0P6kYiejC30LU6SQ4fUDtfSrorxHlp6rD/s5SPaeypjiJ5mzQYx/BfvZw==", "cpu": [ "x64" ], diff --git a/packages/cli/package.json b/packages/cli/package.json index 700a7b9..7d5aafe 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -56,7 +56,7 @@ }, "dependencies": { "@akal/pg-conformance": "~0.0.16", - "@dbdiff/cli": "3.0.0-rc.28", + "@dbdiff/cli": "3.0.0-rc.29", "@modelcontextprotocol/sdk": "^1.32.1", "@oclif/core": "^4", "@supabase/supabase-js": "^2.103.0", diff --git a/packages/cli/test/e2e/renames-and-kinds.test.ts b/packages/cli/test/e2e/renames-and-kinds.test.ts new file mode 100644 index 0000000..e42eae8 --- /dev/null +++ b/packages/cli/test/e2e/renames-and-kinds.test.ts @@ -0,0 +1,106 @@ +/** + * What @dbdiff/cli 3.0.0-rc.29 compares, through the CLI. + * + * - A renamed column or table is renamed, keeping its rows, rather than + * dropped and created again. + * - Range types, aggregates, default privileges and replica identity are + * compared and synced. + * - A kind it does not compare is named beside the schema check, not counted + * as drift. + * + * Verdicts are read from the target database. + */ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest' +import { PgHarness } from '../harness/PgHarness.js' +import { describeWithContainers } from '../harness/containers.js' + +const describeE2E = describeWithContainers() + +const titles = (stdout: string): string[] => + JSON.parse(stdout).checks.flatMap((c: { issues: Array<{ title: string }> }) => c.issues.map(i => i.title)) + +describeE2E('e2e: renames, more kinds, and what is not compared', () => { + let h: PgHarness + + beforeAll(async () => { + h = new PgHarness({ verbose: !!process.env.E2E_VERBOSE, keep: !!process.env.E2E_KEEP }) + await h.up() + }, 300_000) + + afterAll(async () => { await h?.down() }, 120_000) + + beforeEach(async () => { + for (const role of ['source', 'target'] as const) { + await h.applySql(role, 'DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;') + } + }, 60_000) + + it('renames a column and a table, and their rows stay', async () => { + await h.applySql('source', ` + CREATE TABLE public.people (id int PRIMARY KEY, full_name text NOT NULL); + CREATE TABLE public.customers (id int PRIMARY KEY, email text);`) + await h.applySql('target', ` + CREATE TABLE public.people (id int PRIMARY KEY, name text NOT NULL); + CREATE TABLE public.clients (id int PRIMARY KEY, email text); + INSERT INTO public.people SELECT i, 'p' || i FROM generate_series(1, 3) i; + INSERT INTO public.clients SELECT i, i || '@example.com' FROM generate_series(1, 4) i;`) + + const ws = await h.workspace() + const plan = await h.cli(['diff', '--check', 'schema', '--json'], { cwd: ws }) + expect(titles(plan.stdout)).toEqual(expect.arrayContaining([ + 'Column renamed: public.people.name → full_name', + 'Table renamed: public.clients → customers', + ])) + + const r = await h.cli(['diff', '--check', 'schema', '--apply'], { cwd: ws }) + expect(r.code, r.stdout + r.stderr).toBe(0) + expect(await h.sql('target', 'SELECT count(full_name) FROM public.people')).toBe('3') + expect(await h.sql('target', 'SELECT count(*) FROM public.customers')).toBe('4') + + const again = await h.cli(['diff', '--check', 'schema', '--json'], { cwd: ws }) + expect(titles(again.stdout)).toEqual([]) + }, 300_000) + + it('syncs a range type, an aggregate, default privileges and replica identity', async () => { + await h.applySql('source', ` + CREATE TYPE public.floatrange AS RANGE (subtype = float8, subtype_diff = float8mi); + CREATE FUNCTION public.add2(int, int) RETURNS int LANGUAGE sql IMMUTABLE AS 'SELECT $1 + $2'; + CREATE AGGREGATE public.total(int) (SFUNC = public.add2, STYPE = int, INITCOND = '0'); + CREATE TABLE public.events (id int PRIMARY KEY, kind text); + ALTER TABLE public.events REPLICA IDENTITY FULL; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO PUBLIC;`) + await h.applySql('target', 'CREATE TABLE public.events (id int PRIMARY KEY, kind text);') + + const ws = await h.workspace() + const r = await h.cli(['diff', '--check', 'schema', '--apply'], { cwd: ws }) + expect(r.code, r.stdout + r.stderr).toBe(0) + + expect(await h.sql('target', `SELECT to_regtype('public.floatrange') IS NOT NULL`)).toBe('t') + expect(await h.sql('target', 'SELECT public.total(x) FROM generate_series(1, 4) x')).toBe('10') + expect(await h.sql('target', `SELECT relreplident FROM pg_class WHERE oid = 'public.events'::regclass`)).toBe('f') + await h.applySql('target', 'CREATE TABLE public.after_sync (id int);') + expect(await h.sql('target', `SELECT has_table_privilege('public', 'public.after_sync', 'SELECT')`)).toBe('t') + await h.applySql('target', 'DROP TABLE public.after_sync;') + + const again = await h.cli(['diff', '--check', 'schema', '--json'], { cwd: ws }) + expect(titles(again.stdout)).toEqual([]) + }, 300_000) + + it('names an operator under the schema check without counting it', async () => { + await h.applySql('source', ` + CREATE FUNCTION public.same_parity(int, int) RETURNS bool LANGUAGE sql IMMUTABLE AS 'SELECT $1 % 2 = $2 % 2'; + CREATE OPERATOR public.=== (LEFTARG = int, RIGHTARG = int, FUNCTION = public.same_parity);`) + await h.applySql('target', ` + CREATE FUNCTION public.same_parity(int, int) RETURNS bool LANGUAGE sql IMMUTABLE AS 'SELECT $1 % 2 = $2 % 2';`) + + const ws = await h.workspace() + const r = await h.cli(['diff', '--check', 'schema', '--json'], { cwd: ws }) + const schema = JSON.parse(r.stdout).checks[0] + expect(schema.issues).toEqual([]) + expect(schema.status).toBe('clean') + expect(schema.notices).toEqual([expect.stringMatching(/^Not compared: Operator \(source 1, target 0: ===\)/)]) + + const text = await h.cli(['diff', '--check', 'schema'], { cwd: ws }) + expect(text.stdout).toMatch(/ⓘ Not compared: Operator/) + }, 300_000) +})