Skip to content

Commit d8250aa

Browse files
authored
Merge pull request #328 from constructive-io/feat/scripts-extended-inverses
feat(scripts): extend revert/verify vocabulary + node-level AST API
2 parents f888a77 + 948e910 commit d8250aa

11 files changed

Lines changed: 1228 additions & 15 deletions

File tree

__fixtures__/generated/generated.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21220,6 +21220,12 @@
2122021220
"original/alter/alter-table-column-7.sql": "ALTER TABLE public.books\nADD COLUMN tags TEXT[] DEFAULT '{}'",
2122121221
"original/alter/alter-table-column-8.sql": "CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral')",
2122221222
"original/alter/alter-table-column-9.sql": "ALTER TABLE public.profiles\nADD COLUMN current_mood mood DEFAULT 'neutral'",
21223+
"misc/rename-qualified-and-sequence-privs-1.sql": "ALTER TYPE app.t2 RENAME TO t",
21224+
"misc/rename-qualified-and-sequence-privs-2.sql": "ALTER DOMAIN app.d2 RENAME TO d",
21225+
"misc/rename-qualified-and-sequence-privs-3.sql": "GRANT ALL ON SEQUENCE app.seq TO bob",
21226+
"misc/rename-qualified-and-sequence-privs-4.sql": "GRANT USAGE, SELECT ON SEQUENCE app.seq TO bob",
21227+
"misc/rename-qualified-and-sequence-privs-5.sql": "REVOKE ALL ON SEQUENCE app.seq FROM bob",
21228+
"misc/rename-qualified-and-sequence-privs-6.sql": "REVOKE UPDATE ON SEQUENCE app.seq FROM bob RESTRICT",
2122321229
"misc/quotes_etc-1.sql": "CREATE USER MAPPING FOR local_user SERVER \"foreign_server\" OPTIONS (user 'remote_user', password 'secret123')",
2122421230
"misc/quotes_etc-2.sql": "CREATE USER MAPPING FOR local_user SERVER foreign_server OPTIONS (user 'remote_user', password 'secret123')",
2122521231
"misc/quotes_etc-3.sql": "SELECT E'Line 1\\nLine 2'",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Qualified ALTER TYPE / ALTER DOMAIN RENAME must dot-join the name
2+
-- Ref: constructive-io/pgsql-parser#328
3+
ALTER TYPE app.t2 RENAME TO t;
4+
ALTER DOMAIN app.d2 RENAME TO d;
5+
6+
-- GRANT/REVOKE ON SEQUENCE must keep the SEQUENCE keyword
7+
-- Ref: constructive-io/pgsql-parser#328
8+
GRANT ALL ON SEQUENCE app.seq TO bob;
9+
GRANT USAGE, SELECT ON SEQUENCE app.seq TO bob;
10+
REVOKE ALL ON SEQUENCE app.seq FROM bob;
11+
REVOKE UPDATE ON SEQUENCE app.seq FROM bob RESTRICT;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
import { FixtureTestUtils } from '../../test-utils';
3+
const fixtures = new FixtureTestUtils();
4+
5+
it('misc-rename-qualified-and-sequence-privs', async () => {
6+
await fixtures.runFixtureTests([
7+
"misc/rename-qualified-and-sequence-privs-1.sql",
8+
"misc/rename-qualified-and-sequence-privs-2.sql",
9+
"misc/rename-qualified-and-sequence-privs-3.sql",
10+
"misc/rename-qualified-and-sequence-privs-4.sql",
11+
"misc/rename-qualified-and-sequence-privs-5.sql",
12+
"misc/rename-qualified-and-sequence-privs-6.sql"
13+
]);
14+
});

packages/deparser/src/deparser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8419,6 +8419,14 @@ export class Deparser implements DeparserVisitor {
84198419
} else {
84208420
output.push(this.visit(node.object, context));
84218421
}
8422+
} else if ((node.renameType === 'OBJECT_TYPE' || node.renameType === 'OBJECT_DOMAIN') && (node.object as any).List) {
8423+
// Qualified type names - join List parts with dots
8424+
const items = ListUtils.unwrapList(node.object as any);
8425+
const parts = items
8426+
.map((item: any) => item.String?.sval)
8427+
.filter((s: any) => typeof s === 'string')
8428+
.map((s: string) => this.quoteIfNeeded(s));
8429+
output.push(parts.join('.'));
84228430
} else if (node.renameType === 'OBJECT_SCHEMA' && (node.object as any).List) {
84238431
// Handle schema names - extract from List structure
84248432
const items = ListUtils.unwrapList(node.object as any);
@@ -8561,6 +8569,8 @@ export class Deparser implements DeparserVisitor {
85618569
output.push('SCHEMA');
85628570
} else if (node.objtype === 'OBJECT_LANGUAGE') {
85638571
output.push('LANGUAGE');
8572+
} else if (node.objtype === 'OBJECT_SEQUENCE') {
8573+
output.push('SEQUENCE');
85648574
} else if (node.objtype === 'OBJECT_FUNCTION') {
85658575
output.push('FUNCTION');
85668576
} else if (node.objtype === 'OBJECT_PROCEDURE') {

packages/scripts/README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ const { sql: verifySql } = verifyFor(facts);
3030

3131
Nothing outside the supported vocabulary is ever guessed at: `revertFor` emits a `-- revert not derivable: <reason>` comment plus a warning; `verifyFor` emits nothing plus a warning. The list is exported as `SUPPORTED_STATEMENTS` (and `SUPPORTED_NODE_TAGS`).
3232

33+
## Node-level API
34+
35+
For consumers that compose inverses at the AST level (semantic diffing, migration generation) without round-tripping through deparsed text:
36+
37+
```ts
38+
import { invertStatement, existenceCheck } from '@pgsql/scripts';
39+
40+
const inverse = invertStatement(facts[0]); // AST statement nodes, [] = nothing to revert, null = not derivable
41+
const checks = existenceCheck(facts[0]); // SelectStmt check nodes, [] = nothing to check, null = not derivable
42+
```
43+
44+
`invertStatement` returns the per-statement inverse as wrapped AST nodes (e.g. `{ DropStmt: {...} }`); `existenceCheck` returns the raise-on-failure checks as `SelectStmt` nodes. Both return `null` instead of guessing when derivation is not possible — including partially underivable multi-command statements.
45+
3346
## Supported statements
3447

3548
| Statement | Revert | Verify |
@@ -52,5 +65,32 @@ Nothing outside the supported vocabulary is ever guessed at: `revertFor` emits a
5265
| `GRANT` privileges (tables, sequences, functions, schemas) | `REVOKE` same privileges | `has_table_privilege` / `has_function_privilege` / `has_schema_privilege` |
5366
| `GRANT role TO role` | `REVOKE role FROM role` | `pg_auth_members` |
5467
| `COMMENT ON` | `COMMENT ON ... IS NULL` ||
68+
| `CREATE MATERIALIZED VIEW` / `CREATE TABLE AS` | `DROP MATERIALIZED VIEW` / `DROP TABLE` | `to_regclass` |
69+
| `CREATE SERVER` | `DROP SERVER` | `pg_foreign_server` |
70+
| `CREATE FOREIGN TABLE` | `DROP FOREIGN TABLE` | `to_regclass` |
71+
| `CREATE USER MAPPING` | `DROP USER MAPPING` | `pg_user_mappings` |
72+
| `CREATE COLLATION` | `DROP COLLATION` | `pg_collation` |
73+
| `CREATE AGGREGATE` | `DROP AGGREGATE` with input signature | `to_regprocedure` |
74+
| `CREATE OPERATOR` (binary) | `DROP OPERATOR (left, right)` | `to_regoperator` |
75+
| `CREATE CAST` | `DROP CAST (source AS target)` | `pg_cast` |
76+
| `CREATE PUBLICATION` | `DROP PUBLICATION` | `pg_publication` |
77+
| `CREATE SUBSCRIPTION` | `DROP SUBSCRIPTION` | `pg_subscription` |
78+
| `CREATE STATISTICS` | `DROP STATISTICS` | `pg_statistic_ext` |
79+
| `CREATE EVENT TRIGGER` | `DROP EVENT TRIGGER` | `pg_event_trigger` |
80+
| `CREATE RULE` | `DROP RULE ... ON table` | `pg_rules` |
81+
| `ALTER TYPE ... ADD VALUE` | — (Postgres has no `DROP VALUE`; warns) | `pg_enum` |
82+
| `ALTER TABLE ... ATTACH PARTITION` | `DETACH PARTITION` | `pg_inherits` |
83+
| `ALTER DEFAULT PRIVILEGES ... GRANT` | `ALTER DEFAULT PRIVILEGES ... REVOKE` | `pg_default_acl` + `aclexplode` |
84+
| `SECURITY LABEL` | `SECURITY LABEL ... IS NULL` ||
85+
| `CREATE FOREIGN DATA WRAPPER` | `DROP FOREIGN DATA WRAPPER` | `pg_foreign_data_wrapper` |
86+
| `CREATE CONVERSION` | `DROP CONVERSION` | `pg_conversion` |
87+
| `CREATE ACCESS METHOD` | `DROP ACCESS METHOD` | `pg_am` |
88+
| `CREATE TRANSFORM` | `DROP TRANSFORM FOR type LANGUAGE lang` | `pg_transform` |
89+
| `CREATE OPERATOR CLASS` / `FAMILY` | `DROP ... USING am` | `pg_opclass` / `pg_opfamily` |
90+
| `CREATE TEXT SEARCH CONFIGURATION` / `DICTIONARY` / `PARSER` / `TEMPLATE` | matching `DROP` | `pg_ts_config` / `pg_ts_dict` / `pg_ts_parser` / `pg_ts_template` |
91+
| `CREATE TABLESPACE` | `DROP TABLESPACE` | `pg_tablespace` |
92+
| `ALTER ... RENAME TO` | rename back (both names are in the statement) | object exists under new name |
93+
| `ALTER ... SET SCHEMA` (qualified source) | move back (both schemas are in the statement) | object exists in new schema |
94+
| `GRANT ALL` | `REVOKE ALL` | expands to the object type's concrete privilege list |
5595

56-
Not derivable (warned, never guessed): `REVOKE`, unnamed constraints, `ALTER ... SET` with unknown prior value, arbitrary DML, dynamic SQL.
96+
Not derivable (warned, never guessed): `REVOKE`, unnamed constraints, `ALTER ... SET` with unknown prior value, `ALTER ... OWNER TO` (prior owner unknown), `SET SCHEMA` on unqualified names, arbitrary DML, dynamic SQL, prefix operators.

0 commit comments

Comments
 (0)