@prisma-next/target-postgres 0.3.0-dev.12 → 0.3.0-dev.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +9 -2
- package/dist/control.d.mts +19 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +3677 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-DxB8oZzB.mjs +13 -0
- package/dist/descriptor-meta-DxB8oZzB.mjs.map +1 -0
- package/dist/pack.d.mts +10 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +9 -0
- package/dist/pack.mjs.map +1 -0
- package/dist/runtime.d.mts +9 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +21 -0
- package/dist/runtime.mjs.map +1 -0
- package/package.json +30 -29
- package/src/core/migrations/planner-identity-values.ts +129 -0
- package/src/core/migrations/planner-recipes.ts +83 -0
- package/src/core/migrations/planner-reconciliation.ts +781 -0
- package/src/core/migrations/planner-sql.ts +437 -0
- package/src/core/migrations/planner-target-details.ts +16 -0
- package/src/core/migrations/planner.ts +424 -409
- package/src/core/migrations/runner.ts +32 -36
- package/src/core/migrations/statement-builders.ts +9 -7
- package/src/core/types.ts +5 -0
- package/src/exports/control.ts +56 -8
- package/src/exports/pack.ts +5 -2
- package/src/exports/runtime.ts +7 -12
- package/dist/chunk-RKEXRSSI.js +0 -14
- package/dist/chunk-RKEXRSSI.js.map +0 -1
- package/dist/core/descriptor-meta.d.ts +0 -9
- package/dist/core/descriptor-meta.d.ts.map +0 -1
- package/dist/core/migrations/planner.d.ts +0 -14
- package/dist/core/migrations/planner.d.ts.map +0 -1
- package/dist/core/migrations/runner.d.ts +0 -8
- package/dist/core/migrations/runner.d.ts.map +0 -1
- package/dist/core/migrations/statement-builders.d.ts +0 -30
- package/dist/core/migrations/statement-builders.d.ts.map +0 -1
- package/dist/exports/control.d.ts +0 -8
- package/dist/exports/control.d.ts.map +0 -1
- package/dist/exports/control.js +0 -1255
- package/dist/exports/control.js.map +0 -1
- package/dist/exports/pack.d.ts +0 -4
- package/dist/exports/pack.d.ts.map +0 -1
- package/dist/exports/pack.js +0 -11
- package/dist/exports/pack.js.map +0 -1
- package/dist/exports/runtime.d.ts +0 -12
- package/dist/exports/runtime.d.ts.map +0 -1
- package/dist/exports/runtime.js +0 -19
- package/dist/exports/runtime.js.map +0 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { escapeLiteral, quoteIdentifier } from '@prisma-next/adapter-postgres/control';
|
|
2
|
+
import { isTaggedBigInt } from '@prisma-next/contract/types';
|
|
3
|
+
import type { CodecControlHooks } from '@prisma-next/family-sql/control';
|
|
4
|
+
import type {
|
|
5
|
+
ForeignKey,
|
|
6
|
+
ReferentialAction,
|
|
7
|
+
StorageColumn,
|
|
8
|
+
StorageTable,
|
|
9
|
+
} from '@prisma-next/sql-contract/types';
|
|
10
|
+
import type { PostgresColumnDefault } from '../types';
|
|
11
|
+
|
|
12
|
+
export function buildCreateTableSql(
|
|
13
|
+
qualifiedTableName: string,
|
|
14
|
+
table: StorageTable,
|
|
15
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
16
|
+
): string {
|
|
17
|
+
const columnDefinitions = Object.entries(table.columns).map(
|
|
18
|
+
([columnName, column]: [string, StorageColumn]) => {
|
|
19
|
+
const parts = [
|
|
20
|
+
quoteIdentifier(columnName),
|
|
21
|
+
buildColumnTypeSql(column, codecHooks),
|
|
22
|
+
buildColumnDefaultSql(column.default, column),
|
|
23
|
+
column.nullable ? '' : 'NOT NULL',
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
return parts.join(' ');
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const constraintDefinitions: string[] = [];
|
|
30
|
+
if (table.primaryKey) {
|
|
31
|
+
constraintDefinitions.push(
|
|
32
|
+
`PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const allDefinitions = [...columnDefinitions, ...constraintDefinitions];
|
|
37
|
+
return `CREATE TABLE ${qualifiedTableName} (\n ${allDefinitions.join(',\n ')}\n)`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pattern for safe PostgreSQL type names.
|
|
42
|
+
* Allows letters, digits, underscores, spaces (for "double precision", "character varying"),
|
|
43
|
+
* and trailing [] for array types.
|
|
44
|
+
*/
|
|
45
|
+
const SAFE_NATIVE_TYPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9_ ]*(\[\])?$/;
|
|
46
|
+
|
|
47
|
+
function assertSafeNativeType(nativeType: string): void {
|
|
48
|
+
if (!SAFE_NATIVE_TYPE_PATTERN.test(nativeType)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unsafe native type name in contract: "${nativeType}". ` +
|
|
51
|
+
'Native type names must match /^[a-zA-Z][a-zA-Z0-9_ ]*(\\[\\])?$/',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sanity check against accidental SQL injection from malformed contract files.
|
|
58
|
+
* Rejects semicolons, SQL comment tokens, and dollar-quoting.
|
|
59
|
+
* Not a comprehensive security boundary — the contract is developer-authored.
|
|
60
|
+
*/
|
|
61
|
+
function assertSafeDefaultExpression(expression: string): void {
|
|
62
|
+
if (expression.includes(';') || /--|\/\*|\$\$|\bSELECT\b/i.test(expression)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unsafe default expression in contract: "${expression}". ` +
|
|
65
|
+
'Default expressions must not contain semicolons, SQL comment tokens, dollar-quoting, or subqueries.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildColumnTypeSql(
|
|
71
|
+
column: StorageColumn,
|
|
72
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
73
|
+
): string {
|
|
74
|
+
const columnDefault = column.default;
|
|
75
|
+
|
|
76
|
+
if (columnDefault?.kind === 'function' && columnDefault.expression === 'autoincrement()') {
|
|
77
|
+
if (column.nativeType === 'int4' || column.nativeType === 'integer') {
|
|
78
|
+
return 'SERIAL';
|
|
79
|
+
}
|
|
80
|
+
if (column.nativeType === 'int8' || column.nativeType === 'bigint') {
|
|
81
|
+
return 'BIGSERIAL';
|
|
82
|
+
}
|
|
83
|
+
if (column.nativeType === 'int2' || column.nativeType === 'smallint') {
|
|
84
|
+
return 'SMALLSERIAL';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (column.typeRef) {
|
|
89
|
+
return quoteIdentifier(column.nativeType);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
assertSafeNativeType(column.nativeType);
|
|
93
|
+
return renderParameterizedTypeSql(column, codecHooks) ?? column.nativeType;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderParameterizedTypeSql(
|
|
97
|
+
column: StorageColumn,
|
|
98
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
99
|
+
): string | null {
|
|
100
|
+
if (!column.typeParams) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!column.codecId) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Column declares typeParams for nativeType "${column.nativeType}" but has no codecId. ` +
|
|
107
|
+
'Ensure the column is associated with a codec.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hooks = codecHooks.get(column.codecId);
|
|
112
|
+
if (!hooks?.expandNativeType) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Column declares typeParams for nativeType "${column.nativeType}" ` +
|
|
115
|
+
`but no expandNativeType hook is registered for codecId "${column.codecId}". ` +
|
|
116
|
+
'Ensure the extension providing this codec is included in extensionPacks.',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const expanded = hooks.expandNativeType({
|
|
121
|
+
nativeType: column.nativeType,
|
|
122
|
+
codecId: column.codecId,
|
|
123
|
+
typeParams: column.typeParams,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return expanded !== column.nativeType ? expanded : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildColumnDefaultSql(
|
|
130
|
+
columnDefault: PostgresColumnDefault | undefined,
|
|
131
|
+
column?: StorageColumn,
|
|
132
|
+
): string {
|
|
133
|
+
if (!columnDefault) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
switch (columnDefault.kind) {
|
|
138
|
+
case 'literal':
|
|
139
|
+
return `DEFAULT ${renderDefaultLiteral(columnDefault.value, column)}`;
|
|
140
|
+
case 'function': {
|
|
141
|
+
if (columnDefault.expression === 'autoincrement()') {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
assertSafeDefaultExpression(columnDefault.expression);
|
|
145
|
+
return `DEFAULT (${columnDefault.expression})`;
|
|
146
|
+
}
|
|
147
|
+
case 'sequence':
|
|
148
|
+
return `DEFAULT nextval(${quoteIdentifier(columnDefault.name)}::regclass)`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderDefaultLiteral(value: unknown, column?: StorageColumn): string {
|
|
153
|
+
const isJsonColumn = column?.nativeType === 'json' || column?.nativeType === 'jsonb';
|
|
154
|
+
|
|
155
|
+
if (value instanceof Date) {
|
|
156
|
+
return `'${escapeLiteral(value.toISOString())}'`;
|
|
157
|
+
}
|
|
158
|
+
if (!isJsonColumn && isTaggedBigInt(value)) {
|
|
159
|
+
if (!/^-?\d+$/.test(value.value)) {
|
|
160
|
+
throw new Error(`Invalid tagged bigint value: "${value.value}" is not a valid integer`);
|
|
161
|
+
}
|
|
162
|
+
return value.value;
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === 'bigint') {
|
|
165
|
+
return value.toString();
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
return `'${escapeLiteral(value)}'`;
|
|
169
|
+
}
|
|
170
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
171
|
+
return String(value);
|
|
172
|
+
}
|
|
173
|
+
if (value === null) {
|
|
174
|
+
return 'NULL';
|
|
175
|
+
}
|
|
176
|
+
const json = JSON.stringify(value);
|
|
177
|
+
if (isJsonColumn) {
|
|
178
|
+
return `'${escapeLiteral(json)}'::${column.nativeType}`;
|
|
179
|
+
}
|
|
180
|
+
return `'${escapeLiteral(json)}'`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function qualifyTableName(schema: string, table: string): string {
|
|
184
|
+
return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function toRegclassLiteral(schema: string, name: string): string {
|
|
188
|
+
const regclass = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
189
|
+
return `'${escapeLiteral(regclass)}'`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function constraintExistsCheck({
|
|
193
|
+
constraintName,
|
|
194
|
+
schema,
|
|
195
|
+
table,
|
|
196
|
+
exists = true,
|
|
197
|
+
}: {
|
|
198
|
+
constraintName: string;
|
|
199
|
+
schema: string;
|
|
200
|
+
table: string;
|
|
201
|
+
exists?: boolean;
|
|
202
|
+
}): string {
|
|
203
|
+
const existsClause = exists ? 'EXISTS' : 'NOT EXISTS';
|
|
204
|
+
return `SELECT ${existsClause} (
|
|
205
|
+
SELECT 1 FROM pg_constraint c
|
|
206
|
+
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
207
|
+
WHERE c.conname = '${escapeLiteral(constraintName)}'
|
|
208
|
+
AND n.nspname = '${escapeLiteral(schema)}'
|
|
209
|
+
AND c.conrelid = to_regclass(${toRegclassLiteral(schema, table)})
|
|
210
|
+
)`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function columnExistsCheck({
|
|
214
|
+
schema,
|
|
215
|
+
table,
|
|
216
|
+
column,
|
|
217
|
+
exists = true,
|
|
218
|
+
}: {
|
|
219
|
+
schema: string;
|
|
220
|
+
table: string;
|
|
221
|
+
column: string;
|
|
222
|
+
exists?: boolean;
|
|
223
|
+
}): string {
|
|
224
|
+
const existsClause = exists ? '' : 'NOT ';
|
|
225
|
+
return `SELECT ${existsClause}EXISTS (
|
|
226
|
+
SELECT 1
|
|
227
|
+
FROM information_schema.columns
|
|
228
|
+
WHERE table_schema = '${escapeLiteral(schema)}'
|
|
229
|
+
AND table_name = '${escapeLiteral(table)}'
|
|
230
|
+
AND column_name = '${escapeLiteral(column)}'
|
|
231
|
+
)`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function columnNullabilityCheck({
|
|
235
|
+
schema,
|
|
236
|
+
table,
|
|
237
|
+
column,
|
|
238
|
+
nullable,
|
|
239
|
+
}: {
|
|
240
|
+
schema: string;
|
|
241
|
+
table: string;
|
|
242
|
+
column: string;
|
|
243
|
+
nullable: boolean;
|
|
244
|
+
}): string {
|
|
245
|
+
const expected = nullable ? 'YES' : 'NO';
|
|
246
|
+
return `SELECT EXISTS (
|
|
247
|
+
SELECT 1
|
|
248
|
+
FROM information_schema.columns
|
|
249
|
+
WHERE table_schema = '${escapeLiteral(schema)}'
|
|
250
|
+
AND table_name = '${escapeLiteral(table)}'
|
|
251
|
+
AND column_name = '${escapeLiteral(column)}'
|
|
252
|
+
AND is_nullable = '${expected}'
|
|
253
|
+
)`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Maps contract native type names to the display form returned by PostgreSQL's
|
|
258
|
+
* `format_type()`. Base types use short names in the contract (e.g., `int4`)
|
|
259
|
+
* but `format_type()` returns SQL-standard names (e.g., `integer`).
|
|
260
|
+
*
|
|
261
|
+
* NOTE: The inverse mapping lives in `normalizeFormattedType` in control-adapter.ts.
|
|
262
|
+
* These two maps must stay in sync. A shared bidirectional map in
|
|
263
|
+
* @prisma-next/adapter-postgres would eliminate the drift risk.
|
|
264
|
+
*/
|
|
265
|
+
const FORMAT_TYPE_DISPLAY: ReadonlyMap<string, string> = new Map([
|
|
266
|
+
['int2', 'smallint'],
|
|
267
|
+
['int4', 'integer'],
|
|
268
|
+
['int8', 'bigint'],
|
|
269
|
+
['float4', 'real'],
|
|
270
|
+
['float8', 'double precision'],
|
|
271
|
+
['bool', 'boolean'],
|
|
272
|
+
['timestamp', 'timestamp without time zone'],
|
|
273
|
+
['timestamptz', 'timestamp with time zone'],
|
|
274
|
+
['time', 'time without time zone'],
|
|
275
|
+
['timetz', 'time with time zone'],
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Builds the string that `format_type(atttypid, atttypmod)` would return for a
|
|
280
|
+
* contract column. Used for postchecks — separate from `buildColumnTypeSql` which
|
|
281
|
+
* produces DDL-safe strings (e.g., quoted identifiers, SERIAL).
|
|
282
|
+
*/
|
|
283
|
+
export function buildExpectedFormatType(
|
|
284
|
+
column: StorageColumn,
|
|
285
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
286
|
+
): string {
|
|
287
|
+
// Parameterized types: expand with typeParams.
|
|
288
|
+
// format_type() returns the same form (e.g., 'character varying(255)').
|
|
289
|
+
if (column.typeParams && column.codecId) {
|
|
290
|
+
const hooks = codecHooks.get(column.codecId);
|
|
291
|
+
if (hooks?.expandNativeType) {
|
|
292
|
+
return hooks.expandNativeType({
|
|
293
|
+
nativeType: column.nativeType,
|
|
294
|
+
codecId: column.codecId,
|
|
295
|
+
typeParams: column.typeParams,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// User-defined types (enums, composites): format_type() double-quotes names
|
|
301
|
+
// that contain uppercase characters (e.g., "StatusType") but returns lowercase
|
|
302
|
+
// names bare (e.g., status_type). We can't use quoteIdentifier() here because
|
|
303
|
+
// it always quotes, which would break the lowercase case.
|
|
304
|
+
if (column.typeRef) {
|
|
305
|
+
const needsQuoting = column.nativeType !== column.nativeType.toLowerCase();
|
|
306
|
+
return needsQuoting ? `"${column.nativeType}"` : column.nativeType;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Base types: map contract short names to format_type() display names.
|
|
310
|
+
return FORMAT_TYPE_DISPLAY.get(column.nativeType) ?? column.nativeType;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Checks that the column's full type (including typmods) matches the expected type via `format_type()`. */
|
|
314
|
+
export function columnTypeCheck({
|
|
315
|
+
schema,
|
|
316
|
+
table,
|
|
317
|
+
column,
|
|
318
|
+
expectedType,
|
|
319
|
+
}: {
|
|
320
|
+
schema: string;
|
|
321
|
+
table: string;
|
|
322
|
+
column: string;
|
|
323
|
+
expectedType: string;
|
|
324
|
+
}): string {
|
|
325
|
+
return `SELECT EXISTS (
|
|
326
|
+
SELECT 1
|
|
327
|
+
FROM pg_attribute a
|
|
328
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
329
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
330
|
+
WHERE n.nspname = '${escapeLiteral(schema)}'
|
|
331
|
+
AND c.relname = '${escapeLiteral(table)}'
|
|
332
|
+
AND a.attname = '${escapeLiteral(column)}'
|
|
333
|
+
AND format_type(a.atttypid, a.atttypmod) = '${escapeLiteral(expectedType)}'
|
|
334
|
+
AND NOT a.attisdropped
|
|
335
|
+
)`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Checks that a column default exists (or does not exist) via `information_schema.columns.column_default`. */
|
|
339
|
+
export function columnDefaultExistsCheck({
|
|
340
|
+
schema,
|
|
341
|
+
table,
|
|
342
|
+
column,
|
|
343
|
+
exists = true,
|
|
344
|
+
}: {
|
|
345
|
+
schema: string;
|
|
346
|
+
table: string;
|
|
347
|
+
column: string;
|
|
348
|
+
exists?: boolean;
|
|
349
|
+
}): string {
|
|
350
|
+
const nullCheck = exists ? 'IS NOT NULL' : 'IS NULL';
|
|
351
|
+
return `SELECT EXISTS (
|
|
352
|
+
SELECT 1
|
|
353
|
+
FROM information_schema.columns
|
|
354
|
+
WHERE table_schema = '${escapeLiteral(schema)}'
|
|
355
|
+
AND table_name = '${escapeLiteral(table)}'
|
|
356
|
+
AND column_name = '${escapeLiteral(column)}'
|
|
357
|
+
AND column_default ${nullCheck}
|
|
358
|
+
)`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function tableIsEmptyCheck(qualifiedTableName: string): string {
|
|
362
|
+
return `SELECT NOT EXISTS (SELECT 1 FROM ${qualifiedTableName} LIMIT 1)`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function columnHasNoDefaultCheck(opts: {
|
|
366
|
+
schema: string;
|
|
367
|
+
table: string;
|
|
368
|
+
column: string;
|
|
369
|
+
}): string {
|
|
370
|
+
return `SELECT NOT EXISTS (
|
|
371
|
+
SELECT 1
|
|
372
|
+
FROM information_schema.columns
|
|
373
|
+
WHERE table_schema = '${escapeLiteral(opts.schema)}'
|
|
374
|
+
AND table_name = '${escapeLiteral(opts.table)}'
|
|
375
|
+
AND column_name = '${escapeLiteral(opts.column)}'
|
|
376
|
+
AND column_default IS NOT NULL
|
|
377
|
+
)`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function buildAddColumnSql(
|
|
381
|
+
qualifiedTableName: string,
|
|
382
|
+
columnName: string,
|
|
383
|
+
column: StorageColumn,
|
|
384
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
385
|
+
defaultLiteral?: string | null,
|
|
386
|
+
): string {
|
|
387
|
+
const typeSql = buildColumnTypeSql(column, codecHooks);
|
|
388
|
+
const defaultSql =
|
|
389
|
+
buildColumnDefaultSql(column.default, column) ||
|
|
390
|
+
(defaultLiteral != null ? `DEFAULT ${defaultLiteral}` : '');
|
|
391
|
+
const parts = [
|
|
392
|
+
`ALTER TABLE ${qualifiedTableName}`,
|
|
393
|
+
`ADD COLUMN ${quoteIdentifier(columnName)} ${typeSql}`,
|
|
394
|
+
defaultSql,
|
|
395
|
+
column.nullable ? '' : 'NOT NULL',
|
|
396
|
+
].filter(Boolean);
|
|
397
|
+
return parts.join(' ');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
|
|
401
|
+
noAction: 'NO ACTION',
|
|
402
|
+
restrict: 'RESTRICT',
|
|
403
|
+
cascade: 'CASCADE',
|
|
404
|
+
setNull: 'SET NULL',
|
|
405
|
+
setDefault: 'SET DEFAULT',
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export function buildForeignKeySql(
|
|
409
|
+
schemaName: string,
|
|
410
|
+
tableName: string,
|
|
411
|
+
fkName: string,
|
|
412
|
+
foreignKey: ForeignKey,
|
|
413
|
+
): string {
|
|
414
|
+
let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
|
|
415
|
+
ADD CONSTRAINT ${quoteIdentifier(fkName)}
|
|
416
|
+
FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
|
|
417
|
+
REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
|
|
418
|
+
.map(quoteIdentifier)
|
|
419
|
+
.join(', ')})`;
|
|
420
|
+
|
|
421
|
+
if (foreignKey.onDelete !== undefined) {
|
|
422
|
+
const action = REFERENTIAL_ACTION_SQL[foreignKey.onDelete];
|
|
423
|
+
if (!action) {
|
|
424
|
+
throw new Error(`Unknown referential action for onDelete: ${String(foreignKey.onDelete)}`);
|
|
425
|
+
}
|
|
426
|
+
sql += `\nON DELETE ${action}`;
|
|
427
|
+
}
|
|
428
|
+
if (foreignKey.onUpdate !== undefined) {
|
|
429
|
+
const action = REFERENTIAL_ACTION_SQL[foreignKey.onUpdate];
|
|
430
|
+
if (!action) {
|
|
431
|
+
throw new Error(`Unknown referential action for onUpdate: ${String(foreignKey.onUpdate)}`);
|
|
432
|
+
}
|
|
433
|
+
sql += `\nON UPDATE ${action}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return sql;
|
|
437
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
|
+
import type { OperationClass, PostgresPlanTargetDetails } from './planner';
|
|
3
|
+
|
|
4
|
+
export function buildTargetDetails(
|
|
5
|
+
objectType: OperationClass,
|
|
6
|
+
name: string,
|
|
7
|
+
schema: string,
|
|
8
|
+
table?: string,
|
|
9
|
+
): PostgresPlanTargetDetails {
|
|
10
|
+
return {
|
|
11
|
+
schema,
|
|
12
|
+
objectType,
|
|
13
|
+
name,
|
|
14
|
+
...ifDefined('table', table),
|
|
15
|
+
};
|
|
16
|
+
}
|