Skip to content

Commit cfa7fb9

Browse files
committed
refactor(transform): keep only Postgres-native identityOf; move path rendering downstream
1 parent cc6da09 commit cfa7fb9

3 files changed

Lines changed: 41 additions & 138 deletions

File tree

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,47 @@
11
import { loadModule } from 'plpgsql-parser';
22

33
import { classifyStatements } from '../src/facts';
4-
import { changePathFor, identityOf, pathFor } from '../src/naming';
4+
import { identityOf } from '../src/naming';
55

66
beforeAll(async () => {
77
await loadModule();
88
});
99

10-
const pathOf = (sql: string): string | null => changePathFor(classifyStatements(sql)[0]);
11-
12-
describe('PGPM naming spec v1', () => {
13-
it('derives canonical paths per object kind', () => {
14-
expect(pathOf('CREATE SCHEMA app;')).toBe('schemas/app/schema');
15-
expect(pathOf('CREATE TABLE app.users (id int);')).toBe('schemas/app/tables/users/table');
16-
expect(pathOf('CREATE VIEW app.v_users AS SELECT 1;')).toBe('schemas/app/views/v_users');
17-
expect(pathOf('CREATE FUNCTION app.fn() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;'))
18-
.toBe('schemas/app/procedures/fn');
19-
expect(pathOf('CREATE TYPE app.status AS ENUM (\'a\');')).toBe('schemas/app/types/status');
20-
expect(pathOf('CREATE SEQUENCE app.seq;')).toBe('schemas/app/sequences/seq');
21-
expect(pathOf('CREATE EXTENSION pgcrypto;')).toBe('extensions/pgcrypto');
10+
const idOf = (sql: string) => identityOf(classifyStatements(sql)[0]);
11+
12+
describe('identityOf', () => {
13+
it('derives identities per object kind', () => {
14+
expect(idOf('CREATE SCHEMA app;')).toEqual({ kind: 'schema', schema: null, name: 'app' });
15+
expect(idOf('CREATE TABLE app.users (id int);')).toEqual({ kind: 'table', schema: 'app', name: 'users' });
16+
expect(idOf('CREATE VIEW app.v_users AS SELECT 1;')).toEqual({ kind: 'view', schema: 'app', name: 'v_users' });
17+
expect(idOf('CREATE FUNCTION app.fn() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;'))
18+
.toEqual({ kind: 'function', schema: 'app', name: 'fn' });
19+
expect(idOf("CREATE TYPE app.status AS ENUM ('a');")).toEqual({ kind: 'type', schema: 'app', name: 'status' });
20+
expect(idOf('CREATE SEQUENCE app.seq;')).toEqual({ kind: 'sequence', schema: 'app', name: 'seq' });
21+
expect(idOf('CREATE EXTENSION pgcrypto;')).toEqual({ kind: 'extension', schema: null, name: 'pgcrypto' });
2222
});
2323

2424
it('scopes triggers, policies, and indexes to their table', () => {
25-
expect(pathOf(
25+
expect(idOf(
2626
'CREATE TRIGGER trg BEFORE INSERT ON app.users FOR EACH ROW EXECUTE FUNCTION app.fn();'
27-
)).toBe('schemas/app/tables/users/triggers/trg');
28-
expect(pathOf('CREATE POLICY p ON app.users USING (true);'))
29-
.toBe('schemas/app/tables/users/policies/p');
30-
expect(pathOf('CREATE INDEX users_email_idx ON app.users (email);'))
31-
.toBe('schemas/app/tables/users/indexes/users_email_idx');
27+
)).toEqual({ kind: 'trigger', schema: 'app', name: 'trg', table: 'users' });
28+
expect(idOf('CREATE POLICY p ON app.users USING (true);'))
29+
.toEqual({ kind: 'policy', schema: 'app', name: 'p', table: 'users' });
30+
expect(idOf('CREATE INDEX users_email_idx ON app.users (email);'))
31+
.toEqual({ kind: 'index', schema: 'app', name: 'users_email_idx', table: 'users' });
3232
});
3333

34-
it('routes ALTER TABLE constraint statements to the table constraints dir', () => {
35-
const path = pathOf('ALTER TABLE app.users ADD CONSTRAINT users_pkey PRIMARY KEY (id);');
36-
expect(path).toBe('schemas/app/tables/users/constraints/users');
34+
it('targets ALTER TABLE constraint statements at their table', () => {
35+
expect(idOf('ALTER TABLE app.users ADD CONSTRAINT users_pkey PRIMARY KEY (id);'))
36+
.toEqual({ kind: 'constraint', schema: 'app', name: 'users', table: 'users' });
3737
});
3838

3939
it('returns null for statements with no identity of their own', () => {
40-
expect(pathOf('GRANT SELECT ON app.users TO reader;')).toBeNull();
41-
expect(pathOf("COMMENT ON TABLE app.users IS 'x';")).toBeNull();
40+
expect(idOf('GRANT SELECT ON app.users TO reader;')).toBeNull();
41+
expect(idOf("COMMENT ON TABLE app.users IS 'x';")).toBeNull();
4242
});
4343

44-
it('defaults missing schema to public', () => {
45-
expect(pathOf('CREATE TABLE users (id int);')).toBe('schemas/public/tables/users/table');
46-
});
47-
48-
it('pathFor is total and deterministic over identities', () => {
49-
expect(pathFor({ kind: 'role', schema: null, name: 'admin' })).toBe('roles/admin');
50-
expect(pathFor({ kind: 'other', schema: 'app', name: 'thing' })).toBe('schemas/app/objects/thing');
51-
});
52-
53-
it('identity is the key, path is the rendering', () => {
54-
const facts = classifyStatements('CREATE TABLE app.users (id int);')[0];
55-
const identity = identityOf(facts)!;
56-
expect(identity).toEqual({ kind: 'table', schema: 'app', name: 'users' });
57-
expect(pathFor(identity)).toBe('schemas/app/tables/users/table');
44+
it('leaves schema null when unqualified (resolution is a consumer concern)', () => {
45+
expect(idOf('CREATE TABLE users (id int);')).toEqual({ kind: 'table', schema: null, name: 'users' });
5846
});
5947
});

‎packages/transform/src/index.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@ export type {
1717
ObjectIdentity,
1818
ObjectIdentityKind,
1919
} from './naming';
20-
export {
21-
changePathFor,
22-
identityOf,
23-
pathFor,
24-
PGPM_NAMING_SPEC_VERSION,
25-
} from './naming';
20+
export { identityOf } from './naming';
2621
export type {
2722
Granularity,
2823
RestructureOptions,

‎packages/transform/src/naming.ts‎

Lines changed: 14 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,23 @@
11
/**
2-
* PGPM naming spec v1 — canonical, derived change paths.
2+
* Object identity — the canonical, Postgres-native answer to "what object is
3+
* this statement about?".
34
*
4-
* A change path is never authored and never identity: it is a pure projection
5-
* of an object's identity through this spec. Objects (content-addressed ASTs
6-
* + dependency edges) are the source of truth; paths are re-derivable at any
7-
* time, so regrouping, renaming schemes, or repartitioning packages can never
8-
* break identity-keyed consumers (diff, dependency resolution).
5+
* Identity is the key used by dependency graphs, semantic diffing, and any
6+
* downstream naming scheme. It is a pure function of classifier facts —
7+
* grounded in the parser's node taxonomy (`CreateStmt`, `CreateTrigStmt`,
8+
* `IndexStmt`, ...), never in surface syntax like RangeVars. Rendering an
9+
* identity to a change path (e.g. a pgpm module layout) is deliberately NOT
10+
* defined here: paths are derived projections that belong to whichever
11+
* packaging layer consumes the identity, so nothing is ever attached to them.
912
*
1013
* Identity tuple: `(kind, schema, name, table?)` — `table` scopes objects
1114
* that are only unique per table (triggers, policies, indexes, constraints,
12-
* seed data). Function overloads share a path in v1 (disambiguation via a
13-
* signature suffix is reserved for a future spec version).
14-
*
15-
* Canonical templates (matching the conventions used across constructive-db
16-
* deploy trees):
17-
*
18-
* schema schemas/{schema}/schema
19-
* table schemas/{schema}/tables/{table}/table
20-
* trigger schemas/{schema}/tables/{table}/triggers/{name}
21-
* policy schemas/{schema}/tables/{table}/policies/{name}
22-
* index schemas/{schema}/tables/{table}/indexes/{name}
23-
* constraint schemas/{schema}/tables/{table}/constraints/{name}
24-
* seed_dml schemas/{schema}/tables/{table}/fixtures/{name}
25-
* function schemas/{schema}/procedures/{name}
26-
* view schemas/{schema}/views/{name}
27-
* type schemas/{schema}/types/{name}
28-
* sequence schemas/{schema}/sequences/{name}
29-
* extension extensions/{name}
30-
* role roles/{name}
15+
* seed data). Function overloads share an identity for now (signature
16+
* disambiguation is a planned refinement).
3117
*/
3218
import { StatementFacts } from './facts';
3319

34-
/** Spec version, so bundles/modules can declare which scheme derived their paths. */
35-
export const PGPM_NAMING_SPEC_VERSION = 1;
36-
37-
/** The kinds of objects the naming spec assigns paths to. */
20+
/** The kinds of objects an identity can describe. */
3821
export type ObjectIdentityKind =
3922
| 'schema'
4023
| 'extension'
@@ -52,8 +35,8 @@ export type ObjectIdentityKind =
5235
| 'other';
5336

5437
/**
55-
* The identity of a database object — what a change path is derived from.
56-
* Identity is the diff/dependency key; the path is only its rendering.
38+
* The identity of a database object. Identity is the diff/dependency key;
39+
* any path or name is only a downstream rendering of it.
5740
*/
5841
export interface ObjectIdentity {
5942
kind: ObjectIdentityKind;
@@ -65,32 +48,6 @@ export interface ObjectIdentity {
6548
table?: string;
6649
}
6750

68-
/** Kinds whose objects are scoped to (and only unique within) a table. */
69-
const TABLE_SCOPED = new Set<ObjectIdentityKind>([
70-
'trigger',
71-
'policy',
72-
'index',
73-
'constraint',
74-
'seed_dml'
75-
]);
76-
77-
/** Directory names for schema-scoped object kinds. */
78-
const SCHEMA_DIRS: Partial<Record<ObjectIdentityKind, string>> = {
79-
view: 'views',
80-
sequence: 'sequences',
81-
type: 'types',
82-
function: 'procedures'
83-
};
84-
85-
/** Directory names for table-scoped object kinds. */
86-
const TABLE_DIRS: Partial<Record<ObjectIdentityKind, string>> = {
87-
trigger: 'triggers',
88-
policy: 'policies',
89-
index: 'indexes',
90-
constraint: 'constraints',
91-
seed_dml: 'fixtures'
92-
};
93-
9451
/**
9552
* Derive the identity of the object a statement primarily creates or
9653
* targets, or `null` when the statement creates nothing (grants, comments —
@@ -157,40 +114,3 @@ export function identityOf(facts: StatementFacts): ObjectIdentity | null {
157114
return { kind: 'other', schema: created.schema, name: created.name };
158115
}
159116
}
160-
161-
/**
162-
* Render an identity to its canonical pgpm change path (naming spec v1).
163-
* Total: every identity gets a deterministic path.
164-
*/
165-
export function pathFor(identity: ObjectIdentity): string {
166-
const { kind, name } = identity;
167-
const schema = identity.schema ?? 'public';
168-
169-
if (kind === 'schema') return `schemas/${name}/schema`;
170-
if (kind === 'extension') return `extensions/${name}`;
171-
if (kind === 'role') return `roles/${name}`;
172-
if (kind === 'table') return `schemas/${schema}/tables/${name}/table`;
173-
174-
if (TABLE_SCOPED.has(kind)) {
175-
const dir = TABLE_DIRS[kind]!;
176-
if (identity.table && identity.table !== name) {
177-
return `schemas/${schema}/tables/${identity.table}/${dir}/${name}`;
178-
}
179-
// Table-scoped object whose table equals the target (ALTER TABLE
180-
// constraints, seed data keyed by table).
181-
return `schemas/${schema}/tables/${identity.table ?? name}/${dir}/${name}`;
182-
}
183-
184-
const dir = SCHEMA_DIRS[kind];
185-
if (dir) return `schemas/${schema}/${dir}/${name}`;
186-
return `schemas/${schema}/objects/${name}`;
187-
}
188-
189-
/**
190-
* Convenience: canonical change path for a statement, or `null` when the
191-
* statement has no identity of its own.
192-
*/
193-
export function changePathFor(facts: StatementFacts): string | null {
194-
const identity = identityOf(facts);
195-
return identity ? pathFor(identity) : null;
196-
}

0 commit comments

Comments
 (0)