Skip to content

Commit a2dcb13

Browse files
committed
fix(postgres): address PR #260 code review feedback
- Remove unnecessary export from REFERENTIAL_ACTION_SQL - Add blank line between unrelated functions in planner.ts - Trim redundant comments in buildColumnDefaultSql - Document optional table param semantics on constraintExistsCheck - Add unit tests for planner-schema-lookup (16 tests)
1 parent c45c325 commit a2dcb13

3 files changed

Lines changed: 222 additions & 9 deletions

File tree

packages/3-targets/3-targets/postgres/src/core/migrations/planner-ddl-builders.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,7 @@ function renderParameterizedTypeSql(
127127
return expanded !== column.nativeType ? expanded : null;
128128
}
129129

130-
/**
131-
* Builds the DEFAULT clause for a column definition.
132-
* Returns empty string if no default is defined.
133-
*
134-
* Note: autoincrement is handled specially via SERIAL types, so we skip it here.
135-
*/
130+
/** Autoincrement columns use SERIAL types, so this returns empty for them. */
136131
export function buildColumnDefaultSql(
137132
columnDefault: PostgresColumnDefault | undefined,
138133
column?: StorageColumn,
@@ -145,15 +140,13 @@ export function buildColumnDefaultSql(
145140
case 'literal':
146141
return `DEFAULT ${renderDefaultLiteral(columnDefault.value, column)}`;
147142
case 'function': {
148-
// autoincrement is handled by SERIAL type, no explicit DEFAULT needed
149143
if (columnDefault.expression === 'autoincrement()') {
150144
return '';
151145
}
152146
assertSafeDefaultExpression(columnDefault.expression);
153147
return `DEFAULT (${columnDefault.expression})`;
154148
}
155149
case 'sequence':
156-
// Sequence names use quoteIdentifier for safe identifier handling
157150
return `DEFAULT nextval('${escapeLiteral(quoteIdentifier(columnDefault.name))}'::regclass)`;
158151
}
159152
}
@@ -209,7 +202,7 @@ export function buildAddColumnSql(
209202
return parts.join(' ');
210203
}
211204

212-
export const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
205+
const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
213206
noAction: 'NO ACTION',
214207
restrict: 'RESTRICT',
215208
cascade: 'CASCADE',

packages/3-targets/3-targets/postgres/src/core/migrations/planner-sql-checks.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export function toRegclassLiteral(schema: string, name: string): string {
99
return `'${escapeLiteral(regclass)}'`;
1010
}
1111

12+
/**
13+
* When `table` is omitted the check matches by name + schema across all tables.
14+
* Pass `table` to scope the check to a single table (prevents false matches on
15+
* identically-named constraints in different tables).
16+
*/
1217
export function constraintExistsCheck({
1318
constraintName,
1419
schema,
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import type { ForeignKey } from '@prisma-next/sql-contract/types';
2+
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
3+
import { describe, expect, it } from 'vitest';
4+
import {
5+
buildSchemaLookupMap,
6+
hasForeignKey,
7+
hasIndex,
8+
hasUniqueConstraint,
9+
} from '../../src/core/migrations/planner-schema-lookup';
10+
11+
function makeTable(
12+
overrides: Partial<SqlSchemaIR['tables'][string]> = {},
13+
): SqlSchemaIR['tables'][string] {
14+
return {
15+
name: 'test',
16+
columns: {},
17+
foreignKeys: [],
18+
uniques: [],
19+
indexes: [],
20+
...overrides,
21+
};
22+
}
23+
24+
describe('buildSchemaLookupMap', () => {
25+
it('creates a lookup entry for each table', () => {
26+
const schema: SqlSchemaIR = {
27+
tables: {
28+
user: makeTable({ name: 'user' }),
29+
post: makeTable({ name: 'post' }),
30+
},
31+
dependencies: [],
32+
};
33+
const map = buildSchemaLookupMap(schema);
34+
expect(map.size).toBe(2);
35+
expect(map.has('user')).toBe(true);
36+
expect(map.has('post')).toBe(true);
37+
});
38+
39+
it('populates uniqueKeys from uniques', () => {
40+
const schema: SqlSchemaIR = {
41+
tables: {
42+
user: makeTable({
43+
uniques: [{ columns: ['email'] }, { columns: ['tenant', 'slug'] }],
44+
}),
45+
},
46+
dependencies: [],
47+
};
48+
const lookup = buildSchemaLookupMap(schema).get('user')!;
49+
expect(lookup.uniqueKeys.has('email')).toBe(true);
50+
expect(lookup.uniqueKeys.has('tenant,slug')).toBe(true);
51+
});
52+
53+
it('populates indexKeys and uniqueIndexKeys from indexes', () => {
54+
const schema: SqlSchemaIR = {
55+
tables: {
56+
user: makeTable({
57+
indexes: [
58+
{ columns: ['created_at'], unique: false },
59+
{ columns: ['email'], unique: true },
60+
],
61+
}),
62+
},
63+
dependencies: [],
64+
};
65+
const lookup = buildSchemaLookupMap(schema).get('user')!;
66+
expect(lookup.indexKeys.has('created_at')).toBe(true);
67+
expect(lookup.indexKeys.has('email')).toBe(true);
68+
expect(lookup.uniqueIndexKeys.has('email')).toBe(true);
69+
expect(lookup.uniqueIndexKeys.has('created_at')).toBe(false);
70+
});
71+
72+
it('populates fkKeys with pipe-delimited encoding', () => {
73+
const schema: SqlSchemaIR = {
74+
tables: {
75+
post: makeTable({
76+
foreignKeys: [
77+
{ columns: ['author_id'], referencedTable: 'user', referencedColumns: ['id'] },
78+
],
79+
}),
80+
},
81+
dependencies: [],
82+
};
83+
const lookup = buildSchemaLookupMap(schema).get('post')!;
84+
expect(lookup.fkKeys.has('author_id|user|id')).toBe(true);
85+
});
86+
});
87+
88+
describe('hasUniqueConstraint', () => {
89+
const schema: SqlSchemaIR = {
90+
tables: {
91+
user: makeTable({
92+
uniques: [{ columns: ['email'] }],
93+
indexes: [{ columns: ['tenant', 'slug'], unique: true }],
94+
}),
95+
},
96+
dependencies: [],
97+
};
98+
const lookup = buildSchemaLookupMap(schema).get('user')!;
99+
100+
it('matches a declared unique constraint', () => {
101+
expect(hasUniqueConstraint(lookup, ['email'])).toBe(true);
102+
});
103+
104+
it('matches a unique index', () => {
105+
expect(hasUniqueConstraint(lookup, ['tenant', 'slug'])).toBe(true);
106+
});
107+
108+
it('rejects non-matching columns', () => {
109+
expect(hasUniqueConstraint(lookup, ['name'])).toBe(false);
110+
});
111+
112+
it('rejects subset of composite unique columns', () => {
113+
expect(hasUniqueConstraint(lookup, ['tenant'])).toBe(false);
114+
});
115+
});
116+
117+
describe('hasIndex', () => {
118+
const schema: SqlSchemaIR = {
119+
tables: {
120+
user: makeTable({
121+
uniques: [{ columns: ['email'] }],
122+
indexes: [{ columns: ['created_at'], unique: false }],
123+
}),
124+
},
125+
dependencies: [],
126+
};
127+
const lookup = buildSchemaLookupMap(schema).get('user')!;
128+
129+
it('matches a declared index', () => {
130+
expect(hasIndex(lookup, ['created_at'])).toBe(true);
131+
});
132+
133+
it('matches a unique constraint used as an index', () => {
134+
expect(hasIndex(lookup, ['email'])).toBe(true);
135+
});
136+
137+
it('rejects non-matching columns', () => {
138+
expect(hasIndex(lookup, ['name'])).toBe(false);
139+
});
140+
});
141+
142+
describe('hasForeignKey', () => {
143+
const schema: SqlSchemaIR = {
144+
tables: {
145+
post: makeTable({
146+
foreignKeys: [
147+
{ columns: ['author_id'], referencedTable: 'user', referencedColumns: ['id'] },
148+
{
149+
columns: ['org_id', 'team_id'],
150+
referencedTable: 'team',
151+
referencedColumns: ['org_id', 'id'],
152+
},
153+
],
154+
}),
155+
},
156+
dependencies: [],
157+
};
158+
const lookup = buildSchemaLookupMap(schema).get('post')!;
159+
160+
const fk = (
161+
overrides: Partial<ForeignKey> & Pick<ForeignKey, 'columns' | 'references'>,
162+
): ForeignKey => ({
163+
constraint: true,
164+
index: true,
165+
...overrides,
166+
});
167+
168+
it('matches a single-column FK', () => {
169+
expect(
170+
hasForeignKey(
171+
lookup,
172+
fk({ columns: ['author_id'], references: { table: 'user', columns: ['id'] } }),
173+
),
174+
).toBe(true);
175+
});
176+
177+
it('matches a composite FK', () => {
178+
expect(
179+
hasForeignKey(
180+
lookup,
181+
fk({
182+
columns: ['org_id', 'team_id'],
183+
references: { table: 'team', columns: ['org_id', 'id'] },
184+
}),
185+
),
186+
).toBe(true);
187+
});
188+
189+
it('rejects when referenced table differs', () => {
190+
expect(
191+
hasForeignKey(
192+
lookup,
193+
fk({ columns: ['author_id'], references: { table: 'account', columns: ['id'] } }),
194+
),
195+
).toBe(false);
196+
});
197+
198+
it('rejects when referenced columns differ', () => {
199+
expect(
200+
hasForeignKey(
201+
lookup,
202+
fk({ columns: ['author_id'], references: { table: 'user', columns: ['uid'] } }),
203+
),
204+
).toBe(false);
205+
});
206+
207+
it('rejects when source columns differ', () => {
208+
expect(
209+
hasForeignKey(
210+
lookup,
211+
fk({ columns: ['user_id'], references: { table: 'user', columns: ['id'] } }),
212+
),
213+
).toBe(false);
214+
});
215+
});

0 commit comments

Comments
 (0)