@prisma-next/target-postgres 0.3.0-pr.99.6 → 0.4.0-dev.2

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 (66) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +17 -8
  3. package/dist/control.d.mts +19 -0
  4. package/dist/control.d.mts.map +1 -0
  5. package/dist/control.mjs +5382 -0
  6. package/dist/control.mjs.map +1 -0
  7. package/dist/descriptor-meta-CAf16lsJ.mjs +32 -0
  8. package/dist/descriptor-meta-CAf16lsJ.mjs.map +1 -0
  9. package/dist/migration-builders.d.mts +88 -0
  10. package/dist/migration-builders.d.mts.map +1 -0
  11. package/dist/migration-builders.mjs +3 -0
  12. package/dist/operation-descriptors-CxymFSgK.mjs +52 -0
  13. package/dist/operation-descriptors-CxymFSgK.mjs.map +1 -0
  14. package/dist/pack.d.mts +45 -0
  15. package/dist/pack.d.mts.map +1 -0
  16. package/dist/pack.mjs +9 -0
  17. package/dist/pack.mjs.map +1 -0
  18. package/dist/runtime.d.mts +9 -0
  19. package/dist/runtime.d.mts.map +1 -0
  20. package/dist/runtime.mjs +20 -0
  21. package/dist/runtime.mjs.map +1 -0
  22. package/package.json +31 -29
  23. package/src/core/authoring.ts +15 -0
  24. package/src/core/descriptor-meta.ts +5 -0
  25. package/src/core/migrations/descriptor-planner.ts +466 -0
  26. package/src/core/migrations/operation-descriptors.ts +166 -0
  27. package/src/core/migrations/operation-resolver.ts +929 -0
  28. package/src/core/migrations/planner-ddl-builders.ts +256 -0
  29. package/src/core/migrations/planner-identity-values.ts +135 -0
  30. package/src/core/migrations/planner-recipes.ts +91 -0
  31. package/src/core/migrations/planner-reconciliation.ts +798 -0
  32. package/src/core/migrations/planner-schema-lookup.ts +54 -0
  33. package/src/core/migrations/planner-sql-checks.ts +322 -0
  34. package/src/core/migrations/planner-strategies.ts +262 -0
  35. package/src/core/migrations/planner-target-details.ts +38 -0
  36. package/src/core/migrations/planner-type-resolution.ts +26 -0
  37. package/src/core/migrations/planner.ts +410 -460
  38. package/src/core/migrations/runner.ts +134 -38
  39. package/src/core/migrations/statement-builders.ts +6 -6
  40. package/src/core/types.ts +5 -0
  41. package/src/exports/control.ts +182 -12
  42. package/src/exports/migration-builders.ts +56 -0
  43. package/src/exports/pack.ts +7 -3
  44. package/src/exports/runtime.ts +6 -12
  45. package/dist/chunk-RKEXRSSI.js +0 -14
  46. package/dist/chunk-RKEXRSSI.js.map +0 -1
  47. package/dist/core/descriptor-meta.d.ts +0 -9
  48. package/dist/core/descriptor-meta.d.ts.map +0 -1
  49. package/dist/core/migrations/planner.d.ts +0 -14
  50. package/dist/core/migrations/planner.d.ts.map +0 -1
  51. package/dist/core/migrations/runner.d.ts +0 -8
  52. package/dist/core/migrations/runner.d.ts.map +0 -1
  53. package/dist/core/migrations/statement-builders.d.ts +0 -30
  54. package/dist/core/migrations/statement-builders.d.ts.map +0 -1
  55. package/dist/exports/control.d.ts +0 -8
  56. package/dist/exports/control.d.ts.map +0 -1
  57. package/dist/exports/control.js +0 -1260
  58. package/dist/exports/control.js.map +0 -1
  59. package/dist/exports/pack.d.ts +0 -4
  60. package/dist/exports/pack.d.ts.map +0 -1
  61. package/dist/exports/pack.js +0 -11
  62. package/dist/exports/pack.js.map +0 -1
  63. package/dist/exports/runtime.d.ts +0 -12
  64. package/dist/exports/runtime.d.ts.map +0 -1
  65. package/dist/exports/runtime.js +0 -19
  66. package/dist/exports/runtime.js.map +0 -1
@@ -0,0 +1,256 @@
1
+ import { escapeLiteral, quoteIdentifier } from '@prisma-next/adapter-postgres/control';
2
+ import type { CodecControlHooks } from '@prisma-next/family-sql/control';
3
+ import type {
4
+ ForeignKey,
5
+ ReferentialAction,
6
+ StorageColumn,
7
+ StorageTable,
8
+ StorageTypeInstance,
9
+ } from '@prisma-next/sql-contract/types';
10
+ import type { PostgresColumnDefault } from '../types';
11
+ import { qualifyTableName } from './planner-sql-checks';
12
+ import { resolveColumnTypeMetadata } from './planner-type-resolution';
13
+
14
+ export function buildCreateTableSql(
15
+ qualifiedTableName: string,
16
+ table: StorageTable,
17
+ codecHooks: Map<string, CodecControlHooks>,
18
+ storageTypes: Record<string, StorageTypeInstance> = {},
19
+ ): string {
20
+ const columnDefinitions = Object.entries(table.columns).map(
21
+ ([columnName, column]: [string, StorageColumn]) => {
22
+ const parts = [
23
+ quoteIdentifier(columnName),
24
+ buildColumnTypeSql(column, codecHooks, storageTypes),
25
+ buildColumnDefaultSql(column.default, column),
26
+ column.nullable ? '' : 'NOT NULL',
27
+ ].filter(Boolean);
28
+ return parts.join(' ');
29
+ },
30
+ );
31
+
32
+ const constraintDefinitions: string[] = [];
33
+ if (table.primaryKey) {
34
+ constraintDefinitions.push(
35
+ `PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
36
+ );
37
+ }
38
+
39
+ const allDefinitions = [...columnDefinitions, ...constraintDefinitions];
40
+ return `CREATE TABLE ${qualifiedTableName} (\n ${allDefinitions.join(',\n ')}\n)`;
41
+ }
42
+
43
+ /**
44
+ * Pattern for safe PostgreSQL type names.
45
+ * Allows letters, digits, underscores, spaces (for "double precision", "character varying"),
46
+ * and trailing [] for array types.
47
+ */
48
+ const SAFE_NATIVE_TYPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9_ ]*(\[\])?$/;
49
+
50
+ function assertSafeNativeType(nativeType: string): void {
51
+ if (!SAFE_NATIVE_TYPE_PATTERN.test(nativeType)) {
52
+ throw new Error(
53
+ `Unsafe native type name in contract: "${nativeType}". ` +
54
+ 'Native type names must match /^[a-zA-Z][a-zA-Z0-9_ ]*(\\[\\])?$/',
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Sanity check against accidental SQL injection from malformed contract files.
61
+ * Rejects semicolons, SQL comment tokens, and dollar-quoting.
62
+ * Not a comprehensive security boundary — the contract is developer-authored.
63
+ */
64
+ function assertSafeDefaultExpression(expression: string): void {
65
+ if (expression.includes(';') || /--|\/\*|\$\$|\bSELECT\b/i.test(expression)) {
66
+ throw new Error(
67
+ `Unsafe default expression in contract: "${expression}". ` +
68
+ 'Default expressions must not contain semicolons, SQL comment tokens, dollar-quoting, or subqueries.',
69
+ );
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Renders the SQL type for a column in DDL context.
75
+ *
76
+ * @param allowPseudoTypes - When true (default), autoincrement integer columns
77
+ * produce SERIAL/BIGSERIAL/SMALLSERIAL pseudo-types. Set to false for contexts
78
+ * like ALTER COLUMN TYPE where pseudo-types are invalid.
79
+ */
80
+ export function buildColumnTypeSql(
81
+ column: StorageColumn,
82
+ codecHooks: Map<string, CodecControlHooks>,
83
+ storageTypes: Record<string, StorageTypeInstance> = {},
84
+ allowPseudoTypes = true,
85
+ ): string {
86
+ const resolved = resolveColumnTypeMetadata(column, storageTypes);
87
+
88
+ if (allowPseudoTypes) {
89
+ const columnDefault = column.default;
90
+ if (columnDefault?.kind === 'function' && columnDefault.expression === 'autoincrement()') {
91
+ if (resolved.nativeType === 'int4' || resolved.nativeType === 'integer') {
92
+ return 'SERIAL';
93
+ }
94
+ if (resolved.nativeType === 'int8' || resolved.nativeType === 'bigint') {
95
+ return 'BIGSERIAL';
96
+ }
97
+ if (resolved.nativeType === 'int2' || resolved.nativeType === 'smallint') {
98
+ return 'SMALLSERIAL';
99
+ }
100
+ }
101
+ }
102
+
103
+ const expanded = expandParameterizedTypeSql(resolved, codecHooks);
104
+ if (expanded !== null) {
105
+ return expanded;
106
+ }
107
+
108
+ if (column.typeRef) {
109
+ return quoteIdentifier(resolved.nativeType);
110
+ }
111
+
112
+ assertSafeNativeType(resolved.nativeType);
113
+ return resolved.nativeType;
114
+ }
115
+
116
+ function expandParameterizedTypeSql(
117
+ column: Pick<StorageColumn, 'nativeType' | 'codecId' | 'typeParams'>,
118
+ codecHooks: Map<string, CodecControlHooks>,
119
+ ): string | null {
120
+ if (!column.typeParams) {
121
+ return null;
122
+ }
123
+
124
+ if (!column.codecId) {
125
+ throw new Error(
126
+ `Column declares typeParams for nativeType "${column.nativeType}" but has no codecId. ` +
127
+ 'Ensure the column is associated with a codec.',
128
+ );
129
+ }
130
+
131
+ const hooks = codecHooks.get(column.codecId);
132
+ if (!hooks?.expandNativeType) {
133
+ if (hooks?.planTypeOperations) {
134
+ return null;
135
+ }
136
+ throw new Error(
137
+ `Column declares typeParams for nativeType "${column.nativeType}" ` +
138
+ `but no expandNativeType hook is registered for codecId "${column.codecId}". ` +
139
+ 'Ensure the extension providing this codec is included in extensionPacks.',
140
+ );
141
+ }
142
+
143
+ const expanded = hooks.expandNativeType({
144
+ nativeType: column.nativeType,
145
+ codecId: column.codecId,
146
+ typeParams: column.typeParams,
147
+ });
148
+
149
+ return expanded !== column.nativeType ? expanded : null;
150
+ }
151
+
152
+ /** Autoincrement columns use SERIAL types, so this returns empty for them. */
153
+ export function buildColumnDefaultSql(
154
+ columnDefault: PostgresColumnDefault | undefined,
155
+ column?: StorageColumn,
156
+ ): string {
157
+ if (!columnDefault) {
158
+ return '';
159
+ }
160
+
161
+ switch (columnDefault.kind) {
162
+ case 'literal':
163
+ return `DEFAULT ${renderDefaultLiteral(columnDefault.value, column)}`;
164
+ case 'function': {
165
+ if (columnDefault.expression === 'autoincrement()') {
166
+ return '';
167
+ }
168
+ assertSafeDefaultExpression(columnDefault.expression);
169
+ return `DEFAULT (${columnDefault.expression})`;
170
+ }
171
+ case 'sequence':
172
+ return `DEFAULT nextval('${escapeLiteral(quoteIdentifier(columnDefault.name))}'::regclass)`;
173
+ }
174
+ }
175
+
176
+ export function renderDefaultLiteral(value: unknown, column?: StorageColumn): string {
177
+ const isJsonColumn = column?.nativeType === 'json' || column?.nativeType === 'jsonb';
178
+
179
+ if (value instanceof Date) {
180
+ return `'${escapeLiteral(value.toISOString())}'`;
181
+ }
182
+ if (typeof value === 'string') {
183
+ return `'${escapeLiteral(value)}'`;
184
+ }
185
+ if (typeof value === 'number' || typeof value === 'boolean') {
186
+ return String(value);
187
+ }
188
+ if (value === null) {
189
+ return 'NULL';
190
+ }
191
+ const json = JSON.stringify(value);
192
+ if (isJsonColumn) {
193
+ return `'${escapeLiteral(json)}'::${column.nativeType}`;
194
+ }
195
+ return `'${escapeLiteral(json)}'`;
196
+ }
197
+
198
+ export function buildAddColumnSql(
199
+ qualifiedTableName: string,
200
+ columnName: string,
201
+ column: StorageColumn,
202
+ codecHooks: Map<string, CodecControlHooks>,
203
+ temporaryDefault?: string | null,
204
+ storageTypes: Record<string, StorageTypeInstance> = {},
205
+ ): string {
206
+ const typeSql = buildColumnTypeSql(column, codecHooks, storageTypes);
207
+ const defaultSql =
208
+ buildColumnDefaultSql(column.default, column) ||
209
+ (temporaryDefault ? `DEFAULT ${temporaryDefault}` : '');
210
+ const parts = [
211
+ `ALTER TABLE ${qualifiedTableName}`,
212
+ `ADD COLUMN ${quoteIdentifier(columnName)} ${typeSql}`,
213
+ defaultSql,
214
+ column.nullable ? '' : 'NOT NULL',
215
+ ].filter(Boolean);
216
+ return parts.join(' ');
217
+ }
218
+
219
+ const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
220
+ noAction: 'NO ACTION',
221
+ restrict: 'RESTRICT',
222
+ cascade: 'CASCADE',
223
+ setNull: 'SET NULL',
224
+ setDefault: 'SET DEFAULT',
225
+ };
226
+
227
+ export function buildForeignKeySql(
228
+ schemaName: string,
229
+ tableName: string,
230
+ fkName: string,
231
+ foreignKey: ForeignKey,
232
+ ): string {
233
+ let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
234
+ ADD CONSTRAINT ${quoteIdentifier(fkName)}
235
+ FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
236
+ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
237
+ .map(quoteIdentifier)
238
+ .join(', ')})`;
239
+
240
+ if (foreignKey.onDelete !== undefined) {
241
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onDelete];
242
+ if (!action) {
243
+ throw new Error(`Unknown referential action for onDelete: ${String(foreignKey.onDelete)}`);
244
+ }
245
+ sql += `\nON DELETE ${action}`;
246
+ }
247
+ if (foreignKey.onUpdate !== undefined) {
248
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onUpdate];
249
+ if (!action) {
250
+ throw new Error(`Unknown referential action for onUpdate: ${String(foreignKey.onUpdate)}`);
251
+ }
252
+ sql += `\nON UPDATE ${action}`;
253
+ }
254
+
255
+ return sql;
256
+ }
@@ -0,0 +1,135 @@
1
+ import type { CodecControlHooks } from '@prisma-next/family-sql/control';
2
+ import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
4
+
5
+ /**
6
+ * Resolves the identity value (monoid neutral element) as a SQL literal for a column's type.
7
+ * Checks codec hooks first (extensions can provide type-specific identity values),
8
+ * then falls back to the built-in map.
9
+ */
10
+ export function resolveIdentityValue(
11
+ column: StorageColumn,
12
+ codecHooks: Map<string, CodecControlHooks>,
13
+ storageTypes: Record<string, StorageTypeInstance> = {},
14
+ ): string | null {
15
+ const referencedType = column.typeRef ? storageTypes[column.typeRef] : undefined;
16
+ const codecId = referencedType?.codecId ?? column.codecId;
17
+ const nativeType = referencedType?.nativeType ?? column.nativeType;
18
+ const typeParams = referencedType?.typeParams ?? column.typeParams;
19
+
20
+ if (codecId) {
21
+ const hookDefault = codecHooks.get(codecId)?.resolveIdentityValue?.({
22
+ nativeType,
23
+ codecId,
24
+ ...ifDefined('typeParams', typeParams),
25
+ });
26
+ if (hookDefault !== undefined) {
27
+ return hookDefault;
28
+ }
29
+ }
30
+
31
+ return buildBuiltinIdentityValue(nativeType, typeParams);
32
+ }
33
+
34
+ /**
35
+ * Returns the built-in identity value (monoid neutral element) as a SQL literal for the given
36
+ * PostgreSQL native type — e.g. 0 for integers, '' for text, false for booleans.
37
+ *
38
+ * This is the planner's fallback when no codec hook provides a type-specific identity value.
39
+ *
40
+ * Returns null for unrecognized types (for example enums and extension-owned types without a
41
+ * hook), which causes the planner to fall back to the empty-table precheck.
42
+ *
43
+ * @internal Exported for testing only.
44
+ */
45
+ export function buildBuiltinIdentityValue(
46
+ nativeType: string,
47
+ typeParams?: Record<string, unknown>,
48
+ ): string | null {
49
+ const normalizedNativeType = normalizeIdentityValueNativeType(nativeType);
50
+
51
+ if (normalizedNativeType.endsWith('[]')) {
52
+ return "'{}'";
53
+ }
54
+
55
+ switch (normalizedNativeType) {
56
+ case 'text':
57
+ case 'character':
58
+ case 'bpchar':
59
+ case 'character varying':
60
+ case 'varchar':
61
+ return "''";
62
+
63
+ case 'int2':
64
+ case 'int4':
65
+ case 'int8':
66
+ case 'integer':
67
+ case 'bigint':
68
+ case 'smallint':
69
+ case 'float4':
70
+ case 'float8':
71
+ case 'real':
72
+ case 'double precision':
73
+ case 'numeric':
74
+ case 'decimal':
75
+ return '0';
76
+
77
+ case 'bool':
78
+ case 'boolean':
79
+ return 'false';
80
+
81
+ case 'uuid':
82
+ return "'00000000-0000-0000-0000-000000000000'";
83
+
84
+ case 'json':
85
+ return "'{}'::json";
86
+ case 'jsonb':
87
+ return "'{}'::jsonb";
88
+
89
+ case 'date':
90
+ case 'timestamp':
91
+ case 'timestamptz':
92
+ case 'timestamp with time zone':
93
+ case 'timestamp without time zone':
94
+ return "'epoch'";
95
+
96
+ case 'time':
97
+ case 'time without time zone':
98
+ return "'00:00:00'";
99
+ case 'timetz':
100
+ case 'time with time zone':
101
+ return "'00:00:00+00'";
102
+
103
+ case 'interval':
104
+ return "'0'";
105
+
106
+ case 'bytea':
107
+ return "''::bytea";
108
+ case 'tsvector':
109
+ return "''::tsvector";
110
+
111
+ case 'bit':
112
+ return buildBitIdentityValue(typeParams);
113
+ case 'bit varying':
114
+ case 'varbit':
115
+ return "B''";
116
+
117
+ default:
118
+ return null;
119
+ }
120
+ }
121
+
122
+ function normalizeIdentityValueNativeType(nativeType: string): string {
123
+ return nativeType.trim().toLowerCase().replace(/\s+/g, ' ');
124
+ }
125
+
126
+ function buildBitIdentityValue(typeParams?: Record<string, unknown>): string | null {
127
+ const length = typeParams?.['length'];
128
+ if (length === undefined) {
129
+ return "B'0'";
130
+ }
131
+ if (typeof length !== 'number' || !Number.isInteger(length) || length <= 0) {
132
+ return null;
133
+ }
134
+ return `B'${'0'.repeat(length)}'`;
135
+ }
@@ -0,0 +1,91 @@
1
+ import { quoteIdentifier } from '@prisma-next/adapter-postgres/control';
2
+ import type { CodecControlHooks, SqlMigrationPlanOperation } from '@prisma-next/family-sql/control';
3
+ import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
4
+ import { buildAddColumnSql } from './planner-ddl-builders';
5
+ import {
6
+ columnExistsCheck,
7
+ columnHasNoDefaultCheck,
8
+ columnNullabilityCheck,
9
+ qualifyTableName,
10
+ } from './planner-sql-checks';
11
+ import { buildTargetDetails, type PostgresPlanTargetDetails } from './planner-target-details';
12
+
13
+ export function buildAddColumnOperationIdentity(
14
+ schema: string,
15
+ tableName: string,
16
+ columnName: string,
17
+ ): Pick<
18
+ SqlMigrationPlanOperation<PostgresPlanTargetDetails>,
19
+ 'id' | 'label' | 'summary' | 'target'
20
+ > {
21
+ return {
22
+ id: `column.${tableName}.${columnName}`,
23
+ label: `Add column ${columnName} to ${tableName}`,
24
+ summary: `Adds column ${columnName} to table ${tableName}`,
25
+ target: {
26
+ id: 'postgres',
27
+ details: buildTargetDetails('table', tableName, schema),
28
+ },
29
+ };
30
+ }
31
+
32
+ export function buildAddNotNullColumnWithTemporaryDefaultOperation(options: {
33
+ readonly schema: string;
34
+ readonly tableName: string;
35
+ readonly columnName: string;
36
+ readonly column: StorageColumn;
37
+ readonly codecHooks: Map<string, CodecControlHooks>;
38
+ readonly storageTypes: Record<string, StorageTypeInstance>;
39
+ readonly temporaryDefault: string;
40
+ }): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
41
+ const { schema, tableName, columnName, column, codecHooks, storageTypes, temporaryDefault } =
42
+ options;
43
+ const qualified = qualifyTableName(schema, tableName);
44
+
45
+ return {
46
+ ...buildAddColumnOperationIdentity(schema, tableName, columnName),
47
+ operationClass: 'additive',
48
+ precheck: [
49
+ {
50
+ description: `ensure column "${columnName}" is missing`,
51
+ sql: columnExistsCheck({ schema, table: tableName, column: columnName, exists: false }),
52
+ },
53
+ ],
54
+ execute: [
55
+ {
56
+ description: `add column "${columnName}"`,
57
+ sql: buildAddColumnSql(
58
+ qualified,
59
+ columnName,
60
+ column,
61
+ codecHooks,
62
+ temporaryDefault,
63
+ storageTypes,
64
+ ),
65
+ },
66
+ {
67
+ description: `drop temporary default from column "${columnName}"`,
68
+ sql: `ALTER TABLE ${qualified} ALTER COLUMN ${quoteIdentifier(columnName)} DROP DEFAULT`,
69
+ },
70
+ ],
71
+ postcheck: [
72
+ {
73
+ description: `verify column "${columnName}" exists`,
74
+ sql: columnExistsCheck({ schema, table: tableName, column: columnName }),
75
+ },
76
+ {
77
+ description: `verify column "${columnName}" is NOT NULL`,
78
+ sql: columnNullabilityCheck({
79
+ schema,
80
+ table: tableName,
81
+ column: columnName,
82
+ nullable: false,
83
+ }),
84
+ },
85
+ {
86
+ description: `verify column "${columnName}" has no default after temporary default removal`,
87
+ sql: columnHasNoDefaultCheck({ schema, table: tableName, column: columnName }),
88
+ },
89
+ ],
90
+ };
91
+ }