@prisma-next/target-postgres 0.3.0-dev.10 → 0.3.0-dev.113

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.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +9 -2
  3. package/dist/control.d.mts +19 -0
  4. package/dist/control.d.mts.map +1 -0
  5. package/dist/control.mjs +3513 -0
  6. package/dist/control.mjs.map +1 -0
  7. package/dist/descriptor-meta-DxB8oZzB.mjs +13 -0
  8. package/dist/descriptor-meta-DxB8oZzB.mjs.map +1 -0
  9. package/dist/pack.d.mts +10 -0
  10. package/dist/pack.d.mts.map +1 -0
  11. package/dist/pack.mjs +9 -0
  12. package/dist/pack.mjs.map +1 -0
  13. package/dist/runtime.d.mts +9 -0
  14. package/dist/runtime.d.mts.map +1 -0
  15. package/dist/runtime.mjs +21 -0
  16. package/dist/runtime.mjs.map +1 -0
  17. package/package.json +34 -33
  18. package/src/core/migrations/planner-identity-values.ts +129 -0
  19. package/src/core/migrations/planner-recipes.ts +83 -0
  20. package/src/core/migrations/planner-reconciliation.ts +613 -0
  21. package/src/core/migrations/planner-sql.ts +329 -0
  22. package/src/core/migrations/planner-target-details.ts +16 -0
  23. package/src/core/migrations/planner.ts +411 -406
  24. package/src/core/migrations/runner.ts +32 -36
  25. package/src/core/migrations/statement-builders.ts +9 -7
  26. package/src/core/types.ts +5 -0
  27. package/src/exports/control.ts +56 -8
  28. package/src/exports/pack.ts +5 -2
  29. package/src/exports/runtime.ts +7 -12
  30. package/dist/chunk-RKEXRSSI.js +0 -14
  31. package/dist/chunk-RKEXRSSI.js.map +0 -1
  32. package/dist/core/descriptor-meta.d.ts +0 -9
  33. package/dist/core/descriptor-meta.d.ts.map +0 -1
  34. package/dist/core/migrations/planner.d.ts +0 -14
  35. package/dist/core/migrations/planner.d.ts.map +0 -1
  36. package/dist/core/migrations/runner.d.ts +0 -8
  37. package/dist/core/migrations/runner.d.ts.map +0 -1
  38. package/dist/core/migrations/statement-builders.d.ts +0 -30
  39. package/dist/core/migrations/statement-builders.d.ts.map +0 -1
  40. package/dist/exports/control.d.ts +0 -8
  41. package/dist/exports/control.d.ts.map +0 -1
  42. package/dist/exports/control.js +0 -1255
  43. package/dist/exports/control.js.map +0 -1
  44. package/dist/exports/pack.d.ts +0 -4
  45. package/dist/exports/pack.d.ts.map +0 -1
  46. package/dist/exports/pack.js +0 -11
  47. package/dist/exports/pack.js.map +0 -1
  48. package/dist/exports/runtime.d.ts +0 -12
  49. package/dist/exports/runtime.d.ts.map +0 -1
  50. package/dist/exports/runtime.js +0 -19
  51. package/dist/exports/runtime.js.map +0 -1
@@ -0,0 +1,329 @@
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
+ 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
+ exists = true,
196
+ }: {
197
+ constraintName: string;
198
+ schema: string;
199
+ exists?: boolean;
200
+ }): string {
201
+ const existsClause = exists ? 'EXISTS' : 'NOT EXISTS';
202
+ return `SELECT ${existsClause} (
203
+ SELECT 1 FROM pg_constraint c
204
+ JOIN pg_namespace n ON c.connamespace = n.oid
205
+ WHERE c.conname = '${escapeLiteral(constraintName)}'
206
+ AND n.nspname = '${escapeLiteral(schema)}'
207
+ )`;
208
+ }
209
+
210
+ export function columnExistsCheck({
211
+ schema,
212
+ table,
213
+ column,
214
+ exists = true,
215
+ }: {
216
+ schema: string;
217
+ table: string;
218
+ column: string;
219
+ exists?: boolean;
220
+ }): string {
221
+ const existsClause = exists ? '' : 'NOT ';
222
+ return `SELECT ${existsClause}EXISTS (
223
+ SELECT 1
224
+ FROM information_schema.columns
225
+ WHERE table_schema = '${escapeLiteral(schema)}'
226
+ AND table_name = '${escapeLiteral(table)}'
227
+ AND column_name = '${escapeLiteral(column)}'
228
+ )`;
229
+ }
230
+
231
+ export function columnNullabilityCheck({
232
+ schema,
233
+ table,
234
+ column,
235
+ nullable,
236
+ }: {
237
+ schema: string;
238
+ table: string;
239
+ column: string;
240
+ nullable: boolean;
241
+ }): string {
242
+ const expected = nullable ? 'YES' : 'NO';
243
+ return `SELECT EXISTS (
244
+ SELECT 1
245
+ FROM information_schema.columns
246
+ WHERE table_schema = '${escapeLiteral(schema)}'
247
+ AND table_name = '${escapeLiteral(table)}'
248
+ AND column_name = '${escapeLiteral(column)}'
249
+ AND is_nullable = '${expected}'
250
+ )`;
251
+ }
252
+
253
+ export function tableIsEmptyCheck(qualifiedTableName: string): string {
254
+ return `SELECT NOT EXISTS (SELECT 1 FROM ${qualifiedTableName} LIMIT 1)`;
255
+ }
256
+
257
+ export function columnHasNoDefaultCheck(opts: {
258
+ schema: string;
259
+ table: string;
260
+ column: string;
261
+ }): string {
262
+ return `SELECT NOT EXISTS (
263
+ SELECT 1
264
+ FROM information_schema.columns
265
+ WHERE table_schema = '${escapeLiteral(opts.schema)}'
266
+ AND table_name = '${escapeLiteral(opts.table)}'
267
+ AND column_name = '${escapeLiteral(opts.column)}'
268
+ AND column_default IS NOT NULL
269
+ )`;
270
+ }
271
+
272
+ export function buildAddColumnSql(
273
+ qualifiedTableName: string,
274
+ columnName: string,
275
+ column: StorageColumn,
276
+ codecHooks: Map<string, CodecControlHooks>,
277
+ defaultLiteral?: string | null,
278
+ ): string {
279
+ const typeSql = buildColumnTypeSql(column, codecHooks);
280
+ const defaultSql =
281
+ buildColumnDefaultSql(column.default, column) ||
282
+ (defaultLiteral != null ? `DEFAULT ${defaultLiteral}` : '');
283
+ const parts = [
284
+ `ALTER TABLE ${qualifiedTableName}`,
285
+ `ADD COLUMN ${quoteIdentifier(columnName)} ${typeSql}`,
286
+ defaultSql,
287
+ column.nullable ? '' : 'NOT NULL',
288
+ ].filter(Boolean);
289
+ return parts.join(' ');
290
+ }
291
+
292
+ const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
293
+ noAction: 'NO ACTION',
294
+ restrict: 'RESTRICT',
295
+ cascade: 'CASCADE',
296
+ setNull: 'SET NULL',
297
+ setDefault: 'SET DEFAULT',
298
+ };
299
+
300
+ export function buildForeignKeySql(
301
+ schemaName: string,
302
+ tableName: string,
303
+ fkName: string,
304
+ foreignKey: ForeignKey,
305
+ ): string {
306
+ let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
307
+ ADD CONSTRAINT ${quoteIdentifier(fkName)}
308
+ FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
309
+ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
310
+ .map(quoteIdentifier)
311
+ .join(', ')})`;
312
+
313
+ if (foreignKey.onDelete !== undefined) {
314
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onDelete];
315
+ if (!action) {
316
+ throw new Error(`Unknown referential action for onDelete: ${String(foreignKey.onDelete)}`);
317
+ }
318
+ sql += `\nON DELETE ${action}`;
319
+ }
320
+ if (foreignKey.onUpdate !== undefined) {
321
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onUpdate];
322
+ if (!action) {
323
+ throw new Error(`Unknown referential action for onUpdate: ${String(foreignKey.onUpdate)}`);
324
+ }
325
+ sql += `\nON UPDATE ${action}`;
326
+ }
327
+
328
+ return sql;
329
+ }
@@ -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
+ }