@prisma-next/adapter-postgres 0.16.0-dev.25 → 0.16.0-dev.27

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/package.json CHANGED
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "name": "@prisma-next/adapter-postgres",
3
- "version": "0.16.0-dev.25",
3
+ "version": "0.16.0-dev.27",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "dependencies": {
8
- "@prisma-next/contract": "0.16.0-dev.25",
9
- "@prisma-next/contract-authoring": "0.16.0-dev.25",
10
- "@prisma-next/errors": "0.16.0-dev.25",
11
- "@prisma-next/family-sql": "0.16.0-dev.25",
12
- "@prisma-next/framework-components": "0.16.0-dev.25",
13
- "@prisma-next/ids": "0.16.0-dev.25",
14
- "@prisma-next/psl-parser": "0.16.0-dev.25",
15
- "@prisma-next/sql-contract": "0.16.0-dev.25",
16
- "@prisma-next/sql-contract-psl": "0.16.0-dev.25",
17
- "@prisma-next/sql-contract-ts": "0.16.0-dev.25",
18
- "@prisma-next/sql-operations": "0.16.0-dev.25",
19
- "@prisma-next/sql-relational-core": "0.16.0-dev.25",
20
- "@prisma-next/sql-runtime": "0.16.0-dev.25",
21
- "@prisma-next/sql-schema-ir": "0.16.0-dev.25",
22
- "@prisma-next/target-postgres": "0.16.0-dev.25",
23
- "@prisma-next/utils": "0.16.0-dev.25",
8
+ "@prisma-next/contract": "0.16.0-dev.27",
9
+ "@prisma-next/contract-authoring": "0.16.0-dev.27",
10
+ "@prisma-next/errors": "0.16.0-dev.27",
11
+ "@prisma-next/family-sql": "0.16.0-dev.27",
12
+ "@prisma-next/framework-components": "0.16.0-dev.27",
13
+ "@prisma-next/ids": "0.16.0-dev.27",
14
+ "@prisma-next/psl-parser": "0.16.0-dev.27",
15
+ "@prisma-next/sql-contract": "0.16.0-dev.27",
16
+ "@prisma-next/sql-contract-psl": "0.16.0-dev.27",
17
+ "@prisma-next/sql-contract-ts": "0.16.0-dev.27",
18
+ "@prisma-next/sql-operations": "0.16.0-dev.27",
19
+ "@prisma-next/sql-relational-core": "0.16.0-dev.27",
20
+ "@prisma-next/sql-runtime": "0.16.0-dev.27",
21
+ "@prisma-next/sql-schema-ir": "0.16.0-dev.27",
22
+ "@prisma-next/target-postgres": "0.16.0-dev.27",
23
+ "@prisma-next/utils": "0.16.0-dev.27",
24
24
  "arktype": "^2.2.2"
25
25
  },
26
26
  "devDependencies": {
27
- "@prisma-next/cli": "0.16.0-dev.25",
28
- "@prisma-next/driver-postgres": "0.16.0-dev.25",
29
- "@prisma-next/migration-tools": "0.16.0-dev.25",
30
- "@prisma-next/test-utils": "0.16.0-dev.25",
31
- "@prisma-next/tsconfig": "0.16.0-dev.25",
32
- "@prisma-next/tsdown": "0.16.0-dev.25",
27
+ "@prisma-next/cli": "0.16.0-dev.27",
28
+ "@prisma-next/driver-postgres": "0.16.0-dev.27",
29
+ "@prisma-next/migration-tools": "0.16.0-dev.27",
30
+ "@prisma-next/test-utils": "0.16.0-dev.27",
31
+ "@prisma-next/tsconfig": "0.16.0-dev.27",
32
+ "@prisma-next/tsdown": "0.16.0-dev.27",
33
33
  "pathe": "^2.0.3",
34
34
  "tsdown": "0.22.8",
35
35
  "typescript": "5.9.3",
@@ -31,6 +31,7 @@ import type {
31
31
  SqlExecuteRequest,
32
32
  } from '@prisma-next/sql-relational-core/ast';
33
33
  import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
34
+ import { parseWireName } from '@prisma-next/sql-schema-ir/naming';
34
35
  import type {
35
36
  PrimaryKeyInput,
36
37
  SqlCheckConstraintIRInput,
@@ -49,21 +50,24 @@ import type {
49
50
  AddColumnAction,
50
51
  AlterTableActionVisitor,
51
52
  DropDefaultAction,
53
+ PostgresAlterIndexRename,
52
54
  PostgresAlterPolicyRename,
53
55
  PostgresAlterTable,
56
+ PostgresCreateIndex,
54
57
  PostgresCreatePolicy,
55
58
  PostgresCreateSchema,
56
59
  PostgresCreateTable,
57
60
  PostgresCreateType,
58
61
  PostgresDdlNode,
59
62
  PostgresDisableRowLevelSecurity,
63
+ PostgresDropIndex,
60
64
  PostgresDropPolicy,
61
65
  PostgresDropType,
62
66
  RlsPolicyOperation,
63
67
  } from '@prisma-next/target-postgres/ddl';
64
68
  import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
69
+ import { postgresError } from '@prisma-next/target-postgres/errors';
65
70
  import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
66
- import { parseRlsPolicyWireName } from '@prisma-next/target-postgres/rls-canonicalize';
67
71
  import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
68
72
  import {
69
73
  PostgresDatabaseSchemaNode,
@@ -870,8 +874,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
870
874
  tablename: string;
871
875
  indexname: string;
872
876
  indisunique: boolean;
873
- indpartial: boolean;
877
+ where_predicate: string | null;
874
878
  attname: string | null;
879
+ element_def: string | null;
875
880
  index_position: number;
876
881
  amname: string | null;
877
882
  reloptions: string[] | null;
@@ -885,12 +890,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
885
890
  // the table order — verification compares against the contract
886
891
  // with order-sensitive equality and reports a spurious
887
892
  // `index_mismatch`.
893
+ //
894
+ // `element_def` is the per-position element text as Postgres reprints
895
+ // it (`pg_get_indexdef` with pretty-printing); an expression element
896
+ // has attnum 0 so its `attname` is null and the element text is the
897
+ // only faithful capture. A mixed index (columns + expressions) carries
898
+ // the WHOLE element list as one opaque string, so the reprint must
899
+ // fire for every position of an expression-carrying index — the CASE
900
+ // bounds the per-element catalog reconstruction to those indexes,
901
+ // which keeps pure-column schemas free of the reprint cost.
902
+ // `where_predicate` is the reprinted partial-index predicate, null
903
+ // for total indexes.
888
904
  `SELECT
889
905
  i.tablename,
890
906
  i.indexname,
891
907
  ix.indisunique,
892
- ix.indpred IS NOT NULL AS indpartial,
908
+ pg_get_expr(ix.indpred, ix.indrelid) AS where_predicate,
893
909
  a.attname,
910
+ CASE WHEN 0 = ANY(ix.indkey::int[]) THEN pg_get_indexdef(ix.indexrelid, k.ord::int, true) END AS element_def,
894
911
  k.ord AS index_position,
895
912
  am.amname,
896
913
  ic.reloptions
@@ -1128,39 +1145,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1128
1145
  dependsOn: postgresColumnDependsOn(schema, tableName, uq.columns),
1129
1146
  }));
1130
1147
 
1131
- // Process indexes
1148
+ // Process indexes — keyed by catalog-unique index name, so two same-
1149
+ // tuple siblings (a unique index beside a redundant plain index) and
1150
+ // expression indexes all enter the tree at full fidelity.
1132
1151
  const indexesMap = new Map<
1133
1152
  string,
1134
1153
  {
1135
- columns: string[];
1136
1154
  name: string;
1155
+ elements: { attname: string | null; elementDef: string | null }[];
1137
1156
  unique: boolean;
1138
- partial: boolean;
1157
+ where: string | null;
1139
1158
  type: string | undefined;
1140
1159
  options: Record<string, string> | undefined;
1141
1160
  }
1142
1161
  >();
1143
- // An index with an expression key (e.g. `lower(email)`) reports that
1144
- // key's row with `attname = null` (Postgres attribute numbers are
1145
- // <= 0 for expressions, which the LEFT JOIN above can't resolve to a
1146
- // real column). Every row for that index name is skipped below
1147
- // rather than only the expression row, so the index never enters
1148
- // `indexesMap` with a collapsed, misleading column list — a
1149
- // two-column expression index silently reduced to its one real
1150
- // column can coincide with an unrelated real single-column index,
1151
- // and the schema differ's diff-tree node id is derived from the
1152
- // column tuple (`sql-index/index:<columns>`), so two indexes
1153
- // colliding on that tuple abort the diff with "duplicate id among
1154
- // siblings" instead of a normal drift report.
1155
- const indexNamesWithExpressionKey = new Set<string>();
1156
1162
  for (const idxRow of indexesByTable.get(tableName) ?? []) {
1157
- if (!idxRow.attname) {
1158
- indexNamesWithExpressionKey.add(idxRow.indexname);
1159
- continue;
1160
- }
1161
1163
  const existing = indexesMap.get(idxRow.indexname);
1162
1164
  if (existing) {
1163
- existing.columns.push(idxRow.attname);
1165
+ existing.elements.push({ attname: idxRow.attname, elementDef: idxRow.element_def });
1164
1166
  } else {
1165
1167
  // Drop btree (the Postgres default) so a contract index without an
1166
1168
  // explicit type matches a default-method introspected index without
@@ -1168,52 +1170,46 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1168
1170
  const indexType = idxRow.amname && idxRow.amname !== 'btree' ? idxRow.amname : undefined;
1169
1171
  const indexOptions = parsePgReloptions(idxRow.reloptions, idxRow.indexname);
1170
1172
  indexesMap.set(idxRow.indexname, {
1171
- columns: [idxRow.attname],
1172
1173
  name: idxRow.indexname,
1174
+ elements: [{ attname: idxRow.attname, elementDef: idxRow.element_def }],
1173
1175
  unique: idxRow.indisunique,
1174
- partial: idxRow.indpartial,
1176
+ where: idxRow.where_predicate,
1175
1177
  type: indexType,
1176
1178
  options: indexOptions,
1177
1179
  });
1178
1180
  }
1179
1181
  }
1180
- // Two real indexes can legitimately share the exact same column tuple
1181
- // on one table (e.g. a unique index and a redundant plain index) —
1182
- // valid in Postgres, but the schema differ's diff-tree node id for an
1183
- // index is the column tuple alone (`SqlIndexIR#id`, deliberately —
1184
- // see its doc comment), so two same-tuple siblings from one
1185
- // introspection abort the diff with "duplicate id among siblings"
1186
- // rather than a normal drift report. Keep only one per column tuple:
1187
- // the unique one when there is a unique/non-unique pair (a unique
1188
- // index is a strict superset of what a plain index on the same
1189
- // columns would add), otherwise the first by name for determinism.
1190
- const survivingIndexes = Array.from(indexesMap.values()).filter(
1191
- (idx) => !indexNamesWithExpressionKey.has(idx.name),
1192
- );
1193
- const bestByColumnTuple = new Map<string, (typeof survivingIndexes)[number]>();
1194
- for (const idx of survivingIndexes) {
1195
- const tupleKey = idx.columns.join(',');
1196
- const existing = bestByColumnTuple.get(tupleKey);
1197
- if (
1198
- !existing ||
1199
- (idx.unique && !existing.unique) ||
1200
- (idx.unique === existing.unique && idx.name < existing.name)
1201
- ) {
1202
- bestByColumnTuple.set(tupleKey, idx);
1203
- }
1204
- }
1205
- const indexes: readonly SqlIndexIRInput[] = Array.from(bestByColumnTuple.values()).map(
1206
- (idx) => ({
1207
- columns: Object.freeze([...idx.columns]),
1182
+ const indexes: readonly SqlIndexIRInput[] = Array.from(indexesMap.values()).map((idx) => {
1183
+ // An expression element has attnum 0, which the attribute LEFT JOIN
1184
+ // cannot resolve its attname is null. Any such element makes the
1185
+ // whole index an expression node: the entire element list (real
1186
+ // columns included) is carried as one opaque reprinted string.
1187
+ const isExpression = idx.elements.some((el) => el.attname === null);
1188
+ const columnNames = idx.elements.flatMap((el) => (el.attname !== null ? [el.attname] : []));
1189
+ const base = {
1208
1190
  name: idx.name,
1191
+ // Rename-pass grouping only, like policy introspection: undefined
1192
+ // when the live name does not follow the wire-name shape.
1193
+ prefix: parseWireName(idx.name)?.prefix,
1194
+ where: idx.where ?? undefined,
1209
1195
  unique: idx.unique,
1210
- partial: idx.partial,
1196
+ partial: idx.where !== null,
1211
1197
  type: idx.type,
1212
1198
  options: idx.options,
1213
1199
  annotations: undefined,
1214
- dependsOn: postgresColumnDependsOn(schema, tableName, idx.columns),
1215
- }),
1216
- );
1200
+ // Expression indexes stamp chains to every column of the table —
1201
+ // the opaque expression is never parsed, so the deterministic
1202
+ // over-approximation keeps drops ordered.
1203
+ dependsOn: postgresColumnDependsOn(
1204
+ schema,
1205
+ tableName,
1206
+ isExpression ? Object.keys(columns) : columnNames,
1207
+ ),
1208
+ };
1209
+ return isExpression
1210
+ ? { ...base, expression: idx.elements.map((el) => el.elementDef ?? '').join(', ') }
1211
+ : { ...base, columns: Object.freeze([...columnNames]) };
1212
+ });
1217
1213
 
1218
1214
  // Process check constraints — parse each predicate into column + value set.
1219
1215
  // Only the two shapes emitted by this slice are recognised; free-form
@@ -1283,7 +1279,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1283
1279
  ...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
1284
1280
  ].sort();
1285
1281
  const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
1286
- const prefix = parseRlsPolicyWireName(row.policyname)?.prefix ?? row.policyname;
1282
+ const prefix = parseWireName(row.policyname)?.prefix ?? row.policyname;
1287
1283
  const policy = new PostgresPolicySchemaNode({
1288
1284
  name: row.policyname,
1289
1285
  prefix,
@@ -2010,6 +2006,65 @@ function pgRenderAlterPolicyRename(node: PostgresAlterPolicyRename): SqlExecuteR
2010
2006
  };
2011
2007
  }
2012
2008
 
2009
+ /**
2010
+ * Renders one index reloption value: strings single-quote-escaped, finite
2011
+ * numbers verbatim, booleans in the `on`/`off` catalog spelling (the
2012
+ * canonical form the wire hash and the option equality commit to).
2013
+ */
2014
+ function pgRenderIndexOptionValue(key: string, value: unknown): string {
2015
+ if (typeof value === 'string') return `'${escapeLiteral(value)}'`;
2016
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
2017
+ if (typeof value === 'boolean') return value ? 'on' : 'off';
2018
+ throw postgresError(
2019
+ 'CONTRACT.INDEX_INVALID',
2020
+ `Index option "${key}" must be a string, finite number, or boolean; got ${typeof value}`,
2021
+ { meta: { key, valueType: typeof value } },
2022
+ );
2023
+ }
2024
+
2025
+ /** Qualifies an object name; an absent schema renders it unqualified (unbound namespace). */
2026
+ function pgQualify(schema: string | undefined, name: string): string {
2027
+ return schema === undefined
2028
+ ? quoteIdentifier(name)
2029
+ : `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
2030
+ }
2031
+
2032
+ function pgRenderCreateIndex(node: PostgresCreateIndex): SqlExecuteRequest {
2033
+ const elementList =
2034
+ 'columns' in node.elements
2035
+ ? node.elements.columns.map(quoteIdentifier).join(', ')
2036
+ : node.elements.expression;
2037
+ const unique = node.unique ? 'UNIQUE ' : '';
2038
+ const using = node.type !== undefined ? ` USING ${quoteIdentifier(node.type)}` : '';
2039
+ const withClause =
2040
+ node.options !== undefined && Object.keys(node.options).length > 0
2041
+ ? ` WITH (${Object.entries(node.options)
2042
+ .map(
2043
+ ([key, value]) => `${quoteIdentifier(key)} = ${pgRenderIndexOptionValue(key, value)}`,
2044
+ )
2045
+ .join(', ')})`
2046
+ : '';
2047
+ const whereClause = node.where !== undefined ? ` WHERE (${node.where})` : '';
2048
+ return {
2049
+ sql: `CREATE ${unique}INDEX ${quoteIdentifier(node.name)} ON ${pgQualify(node.schema, node.table)}${using} (${elementList})${withClause}${whereClause}`,
2050
+ params: [],
2051
+ };
2052
+ }
2053
+
2054
+ function pgRenderDropIndex(node: PostgresDropIndex): SqlExecuteRequest {
2055
+ return {
2056
+ sql: `DROP INDEX ${pgQualify(node.schema, node.name)}`,
2057
+ params: [],
2058
+ };
2059
+ }
2060
+
2061
+ function pgRenderAlterIndexRename(node: PostgresAlterIndexRename): SqlExecuteRequest {
2062
+ return {
2063
+ sql: `ALTER INDEX ${pgQualify(node.schema, node.from)} RENAME TO ${quoteIdentifier(node.to)}`,
2064
+ params: [],
2065
+ };
2066
+ }
2067
+
2013
2068
  function pgRenderDisableRowLevelSecurity(node: PostgresDisableRowLevelSecurity): SqlExecuteRequest {
2014
2069
  const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
2015
2070
  return {
@@ -2032,6 +2087,10 @@ async function pgRenderDdlExecuteRequest(
2032
2087
  dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
2033
2088
  alterPolicyRename: (node: PostgresAlterPolicyRename) =>
2034
2089
  Promise.resolve(pgRenderAlterPolicyRename(node)),
2090
+ createIndex: (node: PostgresCreateIndex) => Promise.resolve(pgRenderCreateIndex(node)),
2091
+ dropIndex: (node: PostgresDropIndex) => Promise.resolve(pgRenderDropIndex(node)),
2092
+ alterIndexRename: (node: PostgresAlterIndexRename) =>
2093
+ Promise.resolve(pgRenderAlterIndexRename(node)),
2035
2094
  disableRowLevelSecurity: (node: PostgresDisableRowLevelSecurity) =>
2036
2095
  Promise.resolve(pgRenderDisableRowLevelSecurity(node)),
2037
2096
  };