@prisma-next/target-postgres 0.3.0-dev.40 → 0.3.0-dev.43

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/README.md CHANGED
@@ -34,7 +34,7 @@ This package spans multiple planes:
34
34
 
35
35
  This package provides the Postgres implementation of the SQL migration planner/runner used by `prisma-next db init`:
36
36
 
37
- - **Planner** (`src/core/migrations/planner.ts`): produces an additive-only `MigrationPlan` to bring the database schema in line with a destination contract. Extra unrelated schema is tolerated; non-additive mismatches (type/nullability/constraint incompatibilities) surface as structured conflicts. Storage type operations (from codec-owned hooks) are emitted before table operations when `storage.types` are present. The planner respects the contract's `foreignKeys` configuration: when `foreignKeys.constraints` is `false`, FK constraint operations are skipped; when `foreignKeys.indexes` is `false`, FK-backing indexes are omitted. See [ADR 161](../../../docs/architecture%20docs/adrs/ADR%20161%20-%20Explicit%20foreign%20key%20constraint%20and%20index%20configuration.md).
37
+ - **Planner** (`src/core/migrations/planner.ts`): produces an additive-only `MigrationPlan` to bring the database schema in line with a destination contract. Extra unrelated schema is tolerated; non-additive mismatches (type/nullability/constraint incompatibilities) surface as structured conflicts. Storage type operations (from codec-owned hooks) are emitted before table operations when `storage.types` are present. The planner respects the contract's `foreignKeys` configuration: when `foreignKeys.constraints` is `false`, FK constraint operations are skipped; when `foreignKeys.indexes` is `false`, FK-backing indexes are omitted. See [ADR 161](../../../docs/architecture%20docs/adrs/ADR%20161%20-%20Explicit%20foreign%20key%20constraint%20and%20index%20configuration.md). The planner also emits `ON DELETE` and `ON UPDATE` referential action clauses when specified on foreign keys (see [ADR 162](../../../docs/architecture%20docs/adrs/ADR%20162%20-%20Referential%20actions%20for%20foreign%20keys.md)).
38
38
  - **Runner** (`src/core/migrations/runner.ts`): executes a plan under an advisory lock, verifies the post-state schema, then writes the contract marker and appends a ledger entry in the `prisma_contract` schema.
39
39
 
40
40
  For the CLI orchestration, see `packages/1-framework/3-tooling/cli/src/commands/db-init.ts`.
@@ -154,6 +154,7 @@ This package ships a mix of fast planner unit tests and slower runner integratio
154
154
  - **Test files**:
155
155
  - `test/migrations/planner.behavior.test.ts`: Planner unit tests (classification, conflicts, dependency ops)
156
156
  - `test/migrations/planner.fk-config.test.ts`: Planner unit tests for FK constraint/index configuration combinations
157
+ - `test/migrations/planner.referential-actions.test.ts`: Planner unit tests for ON DELETE/ON UPDATE DDL emission
157
158
  - `test/migrations/planner.integration.test.ts`: Planner integration tests
158
159
  - `test/migrations/runner.*.integration.test.ts`: Runner integration tests (basic, errors, idempotency, policy)
159
160
 
@@ -1 +1 @@
1
- {"version":3,"file":"control.d.mts","names":[],"sources":["../src/core/migrations/planner.ts","../src/exports/control.ts"],"sourcesContent":[],"mappings":";;;KAuCK,cAAA;UAuBY,yBAAA;EAvBZ,SAAA,MAAA,EAAc,MAAA;EAuBF,SAAA,UAAA,EAEM,cAFmB;;;;;;cChDpC,0BAA0B,uCAAuC"}
1
+ {"version":3,"file":"control.d.mts","names":[],"sources":["../src/core/migrations/planner.ts","../src/exports/control.ts"],"sourcesContent":[],"mappings":";;;KAyCK,cAAA;UAuBY,yBAAA;EAvBZ,SAAA,MAAA,EAAc,MAAA;EAuBF,SAAA,UAAA,EAEM,cAFmB;;;;;;cClDpC,0BAA0B,uCAAuC"}
package/dist/control.mjs CHANGED
@@ -2,37 +2,13 @@ import { t as postgresTargetDescriptorMeta } from "./descriptor-meta-DxB8oZzB.mj
2
2
  import { SQL_CHAR_CODEC_ID, SQL_FLOAT_CODEC_ID, SQL_INT_CODEC_ID, SQL_VARCHAR_CODEC_ID } from "@prisma-next/sql-relational-core/ast";
3
3
  import { arraysEqual, isIndexSatisfied, isUniqueConstraintSatisfied, verifySqlSchema } from "@prisma-next/family-sql/schema-verify";
4
4
  import { ifDefined } from "@prisma-next/utils/defined";
5
+ import { bigintJsonReplacer, isTaggedBigInt } from "@prisma-next/contract/types";
5
6
  import { createMigrationPlan, extractCodecControlHooks, plannerFailure, plannerSuccess, runnerFailure, runnerSuccess } from "@prisma-next/family-sql/control";
6
7
  import { readMarker } from "@prisma-next/family-sql/verify";
7
8
  import { SqlQueryError } from "@prisma-next/sql-errors";
8
9
  import { ok, okVoid } from "@prisma-next/utils/result";
9
10
 
10
- //#region ../../6-adapters/postgres/dist/codec-ids-Bsm9c7ns.mjs
11
- const PG_TEXT_CODEC_ID = "pg/text@1";
12
- const PG_ENUM_CODEC_ID = "pg/enum@1";
13
- const PG_CHAR_CODEC_ID = "pg/char@1";
14
- const PG_VARCHAR_CODEC_ID = "pg/varchar@1";
15
- const PG_INT_CODEC_ID = "pg/int@1";
16
- const PG_INT2_CODEC_ID = "pg/int2@1";
17
- const PG_INT4_CODEC_ID = "pg/int4@1";
18
- const PG_INT8_CODEC_ID = "pg/int8@1";
19
- const PG_FLOAT_CODEC_ID = "pg/float@1";
20
- const PG_FLOAT4_CODEC_ID = "pg/float4@1";
21
- const PG_FLOAT8_CODEC_ID = "pg/float8@1";
22
- const PG_NUMERIC_CODEC_ID = "pg/numeric@1";
23
- const PG_BOOL_CODEC_ID = "pg/bool@1";
24
- const PG_BIT_CODEC_ID = "pg/bit@1";
25
- const PG_VARBIT_CODEC_ID = "pg/varbit@1";
26
- const PG_TIMESTAMP_CODEC_ID = "pg/timestamp@1";
27
- const PG_TIMESTAMPTZ_CODEC_ID = "pg/timestamptz@1";
28
- const PG_TIME_CODEC_ID = "pg/time@1";
29
- const PG_TIMETZ_CODEC_ID = "pg/timetz@1";
30
- const PG_INTERVAL_CODEC_ID = "pg/interval@1";
31
- const PG_JSON_CODEC_ID = "pg/json@1";
32
- const PG_JSONB_CODEC_ID = "pg/jsonb@1";
33
-
34
- //#endregion
35
- //#region ../../6-adapters/postgres/dist/descriptor-meta-D7pxo-wo.mjs
11
+ //#region ../../6-adapters/postgres/dist/sql-utils-CSfAGEwF.mjs
36
12
  /**
37
13
  * Shared SQL utility functions for the Postgres adapter.
38
14
  *
@@ -106,6 +82,34 @@ function qualifyName(schemaName, objectName) {
106
82
  function validateEnumValueLength(value, enumTypeName) {
107
83
  if (value.length > MAX_IDENTIFIER_LENGTH$1) throw new SqlEscapeError(`Enum value "${value.slice(0, 20)}..." for type "${enumTypeName}" exceeds PostgreSQL's ${MAX_IDENTIFIER_LENGTH$1}-character label limit`, value, "literal");
108
84
  }
85
+
86
+ //#endregion
87
+ //#region ../../6-adapters/postgres/dist/codec-ids-Bsm9c7ns.mjs
88
+ const PG_TEXT_CODEC_ID = "pg/text@1";
89
+ const PG_ENUM_CODEC_ID = "pg/enum@1";
90
+ const PG_CHAR_CODEC_ID = "pg/char@1";
91
+ const PG_VARCHAR_CODEC_ID = "pg/varchar@1";
92
+ const PG_INT_CODEC_ID = "pg/int@1";
93
+ const PG_INT2_CODEC_ID = "pg/int2@1";
94
+ const PG_INT4_CODEC_ID = "pg/int4@1";
95
+ const PG_INT8_CODEC_ID = "pg/int8@1";
96
+ const PG_FLOAT_CODEC_ID = "pg/float@1";
97
+ const PG_FLOAT4_CODEC_ID = "pg/float4@1";
98
+ const PG_FLOAT8_CODEC_ID = "pg/float8@1";
99
+ const PG_NUMERIC_CODEC_ID = "pg/numeric@1";
100
+ const PG_BOOL_CODEC_ID = "pg/bool@1";
101
+ const PG_BIT_CODEC_ID = "pg/bit@1";
102
+ const PG_VARBIT_CODEC_ID = "pg/varbit@1";
103
+ const PG_TIMESTAMP_CODEC_ID = "pg/timestamp@1";
104
+ const PG_TIMESTAMPTZ_CODEC_ID = "pg/timestamptz@1";
105
+ const PG_TIME_CODEC_ID = "pg/time@1";
106
+ const PG_TIMETZ_CODEC_ID = "pg/timetz@1";
107
+ const PG_INTERVAL_CODEC_ID = "pg/interval@1";
108
+ const PG_JSON_CODEC_ID = "pg/json@1";
109
+ const PG_JSONB_CODEC_ID = "pg/jsonb@1";
110
+
111
+ //#endregion
112
+ //#region ../../6-adapters/postgres/dist/descriptor-meta-ilnFI7bx.mjs
109
113
  const ENUM_INTROSPECT_QUERY = `
110
114
  SELECT
111
115
  n.nspname AS schema_name,
@@ -1037,6 +1041,8 @@ const STRING_LITERAL_PATTERN = /^'((?:[^']|'')*)'(?:::(?:"[^"]+"|[\w\s]+)(?:\(\d
1037
1041
  */
1038
1042
  function parsePostgresDefault(rawDefault, _nativeType) {
1039
1043
  const trimmed = rawDefault.trim();
1044
+ const normalizedType = _nativeType?.toLowerCase();
1045
+ const isBigInt = normalizedType === "bigint" || normalizedType === "int8";
1040
1046
  if (NEXTVAL_PATTERN.test(trimmed)) return {
1041
1047
  kind: "function",
1042
1048
  expression: "autoincrement()"
@@ -1055,21 +1061,39 @@ function parsePostgresDefault(rawDefault, _nativeType) {
1055
1061
  };
1056
1062
  if (TRUE_PATTERN.test(trimmed)) return {
1057
1063
  kind: "literal",
1058
- expression: "true"
1064
+ value: true
1059
1065
  };
1060
1066
  if (FALSE_PATTERN.test(trimmed)) return {
1061
1067
  kind: "literal",
1062
- expression: "false"
1063
- };
1064
- if (NUMERIC_PATTERN.test(trimmed)) return {
1065
- kind: "literal",
1066
- expression: trimmed
1068
+ value: false
1067
1069
  };
1070
+ if (NUMERIC_PATTERN.test(trimmed)) {
1071
+ if (isBigInt) return {
1072
+ kind: "literal",
1073
+ value: {
1074
+ $type: "bigint",
1075
+ value: trimmed
1076
+ }
1077
+ };
1078
+ return {
1079
+ kind: "literal",
1080
+ value: Number(trimmed)
1081
+ };
1082
+ }
1068
1083
  const stringMatch = trimmed.match(STRING_LITERAL_PATTERN);
1069
- if (stringMatch?.[1] !== void 0) return {
1070
- kind: "literal",
1071
- expression: `'${stringMatch[1]}'`
1072
- };
1084
+ if (stringMatch?.[1] !== void 0) {
1085
+ const unescaped = stringMatch[1].replace(/''/g, "'");
1086
+ if (normalizedType === "json" || normalizedType === "jsonb") try {
1087
+ return {
1088
+ kind: "literal",
1089
+ value: JSON.parse(unescaped)
1090
+ };
1091
+ } catch {}
1092
+ return {
1093
+ kind: "literal",
1094
+ value: unescaped
1095
+ };
1096
+ }
1073
1097
  return {
1074
1098
  kind: "function",
1075
1099
  expression: trimmed
@@ -1160,17 +1184,31 @@ var PostgresControlAdapter = class {
1160
1184
  tc.constraint_name,
1161
1185
  kcu.column_name,
1162
1186
  kcu.ordinal_position,
1163
- ccu.table_schema AS referenced_table_schema,
1164
- ccu.table_name AS referenced_table_name,
1165
- ccu.column_name AS referenced_column_name
1187
+ ref_ns.nspname AS referenced_table_schema,
1188
+ ref_cl.relname AS referenced_table_name,
1189
+ ref_att.attname AS referenced_column_name,
1190
+ rc.delete_rule,
1191
+ rc.update_rule
1166
1192
  FROM information_schema.table_constraints tc
1167
1193
  JOIN information_schema.key_column_usage kcu
1168
1194
  ON tc.constraint_name = kcu.constraint_name
1169
1195
  AND tc.table_schema = kcu.table_schema
1170
1196
  AND tc.table_name = kcu.table_name
1171
- JOIN information_schema.constraint_column_usage ccu
1172
- ON ccu.constraint_name = tc.constraint_name
1173
- AND ccu.table_schema = tc.table_schema
1197
+ JOIN pg_catalog.pg_constraint pgc
1198
+ ON pgc.conname = tc.constraint_name
1199
+ AND pgc.connamespace = (
1200
+ SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = tc.table_schema
1201
+ )
1202
+ JOIN pg_catalog.pg_class ref_cl
1203
+ ON ref_cl.oid = pgc.confrelid
1204
+ JOIN pg_catalog.pg_namespace ref_ns
1205
+ ON ref_ns.oid = ref_cl.relnamespace
1206
+ JOIN pg_catalog.pg_attribute ref_att
1207
+ ON ref_att.attrelid = pgc.confrelid
1208
+ AND ref_att.attnum = pgc.confkey[kcu.ordinal_position]
1209
+ JOIN information_schema.referential_constraints rc
1210
+ ON rc.constraint_name = tc.constraint_name
1211
+ AND rc.constraint_schema = tc.table_schema
1174
1212
  WHERE tc.table_schema = $1
1175
1213
  AND tc.constraint_type = 'FOREIGN KEY'
1176
1214
  ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`, [schema]),
@@ -1264,14 +1302,18 @@ var PostgresControlAdapter = class {
1264
1302
  columns: [fkRow.column_name],
1265
1303
  referencedTable: fkRow.referenced_table_name,
1266
1304
  referencedColumns: [fkRow.referenced_column_name],
1267
- name: fkRow.constraint_name
1305
+ name: fkRow.constraint_name,
1306
+ deleteRule: fkRow.delete_rule,
1307
+ updateRule: fkRow.update_rule
1268
1308
  });
1269
1309
  }
1270
1310
  const foreignKeys = Array.from(foreignKeysMap.values()).map((fk) => ({
1271
1311
  columns: Object.freeze([...fk.columns]),
1272
1312
  referencedTable: fk.referencedTable,
1273
1313
  referencedColumns: Object.freeze([...fk.referencedColumns]),
1274
- name: fk.name
1314
+ name: fk.name,
1315
+ ...ifDefined("onDelete", mapReferentialAction(fk.deleteRule)),
1316
+ ...ifDefined("onUpdate", mapReferentialAction(fk.updateRule))
1275
1317
  }));
1276
1318
  const pkConstraints = pkConstraintsByTable.get(tableName) ?? /* @__PURE__ */ new Set();
1277
1319
  const uniquesMap = /* @__PURE__ */ new Map();
@@ -1378,6 +1420,24 @@ function normalizeFormattedType(formattedType, dataType, udtName) {
1378
1420
  if (formattedType.startsWith("\"") && formattedType.endsWith("\"")) return formattedType.slice(1, -1);
1379
1421
  return formattedType;
1380
1422
  }
1423
+ const PG_REFERENTIAL_ACTION_MAP = {
1424
+ "NO ACTION": "noAction",
1425
+ RESTRICT: "restrict",
1426
+ CASCADE: "cascade",
1427
+ "SET NULL": "setNull",
1428
+ "SET DEFAULT": "setDefault"
1429
+ };
1430
+ /**
1431
+ * Maps a Postgres referential action rule to the canonical SqlReferentialAction.
1432
+ * Returns undefined for 'NO ACTION' (the database default) to keep the IR sparse.
1433
+ * Throws for unrecognized rules to prevent silent data loss.
1434
+ */
1435
+ function mapReferentialAction(rule) {
1436
+ const mapped = PG_REFERENTIAL_ACTION_MAP[rule];
1437
+ if (mapped === void 0) throw new Error(`Unknown PostgreSQL referential action rule: "${rule}". Expected one of: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT.`);
1438
+ if (mapped === "noAction") return void 0;
1439
+ return mapped;
1440
+ }
1381
1441
  /**
1382
1442
  * Groups an array of objects by a specified key.
1383
1443
  * Returns a Map for O(1) lookup by group key.
@@ -1782,10 +1842,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
1782
1842
  }],
1783
1843
  execute: [{
1784
1844
  description: `add foreign key "${fkName}"`,
1785
- sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
1786
- ADD CONSTRAINT ${quoteIdentifier(fkName)}
1787
- FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(", ")})
1788
- REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns.map(quoteIdentifier).join(", ")})`
1845
+ sql: buildForeignKeySql(schemaName, tableName, fkName, foreignKey)
1789
1846
  }],
1790
1847
  postcheck: [{
1791
1848
  description: `verify foreign key "${fkName}" exists`,
@@ -1875,7 +1932,7 @@ function buildCreateTableSql(qualifiedTableName, table) {
1875
1932
  return [
1876
1933
  quoteIdentifier(columnName),
1877
1934
  buildColumnTypeSql(column),
1878
- buildColumnDefaultSql(column.default),
1935
+ buildColumnDefaultSql(column.default, column),
1879
1936
  column.nullable ? "" : "NOT NULL"
1880
1937
  ].filter(Boolean).join(" ");
1881
1938
  });
@@ -1919,16 +1976,31 @@ function renderParameterizedTypeSql(column) {
1919
1976
  *
1920
1977
  * Note: autoincrement is handled specially via SERIAL types, so we skip it here.
1921
1978
  */
1922
- function buildColumnDefaultSql(columnDefault) {
1979
+ function buildColumnDefaultSql(columnDefault, column) {
1923
1980
  if (!columnDefault) return "";
1924
1981
  switch (columnDefault.kind) {
1925
- case "literal": return `DEFAULT ${columnDefault.expression}`;
1982
+ case "literal": return `DEFAULT ${renderDefaultLiteral(columnDefault.value, column)}`;
1926
1983
  case "function":
1927
1984
  if (columnDefault.expression === "autoincrement()") return "";
1928
1985
  return `DEFAULT ${columnDefault.expression}`;
1929
1986
  case "sequence": return `DEFAULT nextval(${quoteIdentifier(columnDefault.name)}::regclass)`;
1930
1987
  }
1931
1988
  }
1989
+ function renderDefaultLiteral(value, column) {
1990
+ const isJsonColumn = column?.nativeType === "json" || column?.nativeType === "jsonb";
1991
+ if (value instanceof Date) return `'${escapeLiteral(value.toISOString())}'`;
1992
+ if (!isJsonColumn && isTaggedBigInt(value)) {
1993
+ if (!/^-?\d+$/.test(value.value)) throw new Error(`Invalid tagged bigint value: "${value.value}" is not a valid integer`);
1994
+ return value.value;
1995
+ }
1996
+ if (typeof value === "bigint") return value.toString();
1997
+ if (typeof value === "string") return `'${escapeLiteral(value)}'`;
1998
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1999
+ if (value === null) return "NULL";
2000
+ const json = JSON.stringify(value);
2001
+ if (isJsonColumn) return `'${escapeLiteral(json)}'::${column.nativeType}`;
2002
+ return `'${escapeLiteral(json)}'`;
2003
+ }
1932
2004
  function qualifyTableName(schema, table) {
1933
2005
  return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
1934
2006
  }
@@ -1970,7 +2042,7 @@ function tableIsEmptyCheck(qualifiedTableName) {
1970
2042
  }
1971
2043
  function buildAddColumnSql(qualifiedTableName, columnName, column) {
1972
2044
  const typeSql = buildColumnTypeSql(column);
1973
- const defaultSql = buildColumnDefaultSql(column.default);
2045
+ const defaultSql = buildColumnDefaultSql(column.default, column);
1974
2046
  return [
1975
2047
  `ALTER TABLE ${qualifiedTableName}`,
1976
2048
  `ADD COLUMN ${quoteIdentifier(columnName)} ${typeSql}`,
@@ -2049,6 +2121,30 @@ function compareStrings(a, b) {
2049
2121
  if (b === void 0) return 1;
2050
2122
  return a < b ? -1 : 1;
2051
2123
  }
2124
+ const REFERENTIAL_ACTION_SQL = {
2125
+ noAction: "NO ACTION",
2126
+ restrict: "RESTRICT",
2127
+ cascade: "CASCADE",
2128
+ setNull: "SET NULL",
2129
+ setDefault: "SET DEFAULT"
2130
+ };
2131
+ function buildForeignKeySql(schemaName, tableName, fkName, foreignKey) {
2132
+ let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
2133
+ ADD CONSTRAINT ${quoteIdentifier(fkName)}
2134
+ FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(", ")})
2135
+ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns.map(quoteIdentifier).join(", ")})`;
2136
+ if (foreignKey.onDelete !== void 0) {
2137
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onDelete];
2138
+ if (!action) throw new Error(`Unknown referential action for onDelete: ${String(foreignKey.onDelete)}`);
2139
+ sql += `\nON DELETE ${action}`;
2140
+ }
2141
+ if (foreignKey.onUpdate !== void 0) {
2142
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onUpdate];
2143
+ if (!action) throw new Error(`Unknown referential action for onUpdate: ${String(foreignKey.onUpdate)}`);
2144
+ sql += `\nON UPDATE ${action}`;
2145
+ }
2146
+ return sql;
2147
+ }
2052
2148
 
2053
2149
  //#endregion
2054
2150
  //#region src/core/migrations/statement-builders.ts
@@ -2161,7 +2257,7 @@ function buildLedgerInsertStatement(input) {
2161
2257
  };
2162
2258
  }
2163
2259
  function jsonParam(value) {
2164
- return JSON.stringify(value ?? null);
2260
+ return JSON.stringify(value ?? null, bigintJsonReplacer);
2165
2261
  }
2166
2262
 
2167
2263
  //#endregion