Skip to content

Commit 6bc851e

Browse files
committed
feat(pgpm): compose semantic-diff inverses/verifies at the AST level via @pgsql/scripts node API
Tarball-pinned @pgsql/scripts + pgsql-deparser for testing ahead of publish (see .tarballs/); DO NOT MERGE until the tarball deps are replaced with published versions.
1 parent f31cfcd commit 6bc851e

6 files changed

Lines changed: 2578 additions & 7187 deletions

File tree

128 KB
Binary file not shown.

.tarballs/pgsql-scripts-18.0.1.tgz

24.2 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"pnpm": {
5353
"overrides": {
5454
"@dataplan/json": "1.0.0",
55+
"pgsql-deparser": "file:./.tarballs/pgsql-deparser-18.1.1.tgz",
5556
"graphql": "16.13.0",
5657
"@smithy/node-http-handler": "<4.5.0"
5758
},

pgpm/transform/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
},
4545
"dependencies": {
4646
"@pgpmjs/naming-spec": "workspace:^",
47-
"@pgsql/scripts": "^18.0.0",
47+
"@pgsql/scripts": "file:../../.tarballs/pgsql-scripts-18.0.1.tgz",
4848
"@pgsql/transform": "^18.11.0",
4949
"plpgsql-parser": "^18.2.2"
5050
}

pgpm/transform/src/semantic-diff-driver.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@
1616
* modified objects routed through the granularity pipeline
1717
* (`restructureChanges`), so names come from the naming spec and `requires`
1818
* from the statement graph — the same derivation every other projection uses.
19-
* Every emitted change carries a full deploy/revert/verify triple: drops are
20-
* `@pgsql/scripts`' `revertFor` inverses of the object's own deploy
21-
* statements (never hand-templated), reverts recreate what was dropped, and
22-
* verifies come from `verifyFor`.
19+
* Every emitted change carries a full deploy/revert/verify triple: inverses
20+
* and existence checks are composed at the AST level via `@pgsql/scripts`'
21+
* node-level API (`invertStatement`/`existenceCheck`) — never hand-templated
22+
* and never round-tripped through deparsed text — deparsing only once at the
23+
* end. Statements the node layer cannot derive fall back to the text layer
24+
* (`revertFor`/`verifyFor`) solely to surface its not-derivable comment and
25+
* warning.
2326
*
2427
* Requires `loadModule()` from `plpgsql-parser` before use.
2528
*/
2629
import { pathFor, PathStyle } from '@pgpmjs/naming-spec';
27-
import { revertFor, verifyFor } from '@pgsql/scripts';
30+
import { existenceCheck, invertStatement, revertFor, verifyFor } from '@pgsql/scripts';
2831
import type { ObjectIdentity, StatementFacts } from '@pgsql/transform';
2932
import {
3033
buildStatementGraph,
@@ -344,17 +347,45 @@ export function diffSchemas(
344347
const modifiedChanges: SemanticDeltaChange[] = [];
345348
const dropUnits: ObjectUnit[] = [];
346349

347-
/** `@pgsql/scripts` inverse of a deploy script (drops in reverse topo order). */
350+
/**
351+
* `@pgsql/scripts` inverse of a deploy script (drops in reverse topo
352+
* order), composed at the AST level via `invertStatement` and deparsed
353+
* once. Underivable statements fall back to `revertFor` on the single
354+
* statement to surface its not-derivable comment and warning.
355+
*/
348356
const inverseOf = (deploy: string, context: string): string => {
349-
const { sql, warnings: w } = revertFor(classifyStatements(deploy));
350-
warnings.push(...w.map(x => `${context}: ${x}`));
351-
return sql;
357+
const facts = classifyStatements(deploy);
358+
const graph = buildStatementGraph(facts);
359+
const pieces: string[] = [];
360+
for (const i of [...graph.order].reverse()) {
361+
const nodes = invertStatement(facts[i]);
362+
if (nodes === null) {
363+
const { sql, warnings: w } = revertFor([facts[i]]);
364+
warnings.push(...w.map(x => `${context}: ${x}`));
365+
if (sql.trim()) pieces.push(sql);
366+
continue;
367+
}
368+
pieces.push(...nodes.map(n => deparseStmt(n as Record<string, unknown>)));
369+
}
370+
return pieces.join('\n');
352371
};
353-
/** `@pgsql/scripts` existence checks for a deploy script. */
372+
/**
373+
* `@pgsql/scripts` existence checks for a deploy script, composed from
374+
* `existenceCheck`'s SelectStmt nodes and deparsed once. Underivable
375+
* statements fall back to `verifyFor` to surface the warning.
376+
*/
354377
const verifyOf = (deploy: string, context: string): string => {
355-
const { sql, warnings: w } = verifyFor(classifyStatements(deploy));
356-
warnings.push(...w.map(x => `${context}: ${x}`));
357-
return sql;
378+
const pieces: string[] = [];
379+
for (const fact of classifyStatements(deploy)) {
380+
const nodes = existenceCheck(fact);
381+
if (nodes === null) {
382+
const { warnings: w } = verifyFor([fact]);
383+
warnings.push(...w.map(x => `${context}: ${x}`));
384+
continue;
385+
}
386+
pieces.push(...nodes.map(n => deparseStmt(n as Record<string, unknown>)));
387+
}
388+
return pieces.join('\n\n');
358389
};
359390

360391
for (const key of b.order) {

0 commit comments

Comments
 (0)