@prisma-next/adapter-postgres 0.14.0-dev.3 → 0.14.0-dev.30

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.
@@ -1,4 +1,8 @@
1
- import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
1
+ import type {
2
+ Contract,
3
+ ContractMarkerRecord,
4
+ LedgerEntryRecord,
5
+ } from '@prisma-next/contract/types';
2
6
  import {
3
7
  parseMarkerRowSafely,
4
8
  rethrowMarkerReadError,
@@ -7,11 +11,11 @@ import {
7
11
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
8
12
  import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
9
13
  import type { CodecLookup, CodecRegistry } from '@prisma-next/framework-components/codec';
10
- import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
14
+ import { APP_SPACE_ID, type SchemaDiffIssue } from '@prisma-next/framework-components/control';
11
15
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
12
16
  import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
13
17
  import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
14
- import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
18
+ import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-contract/types';
15
19
  import type {
16
20
  AnyQueryAst,
17
21
  CodecRef,
@@ -35,7 +39,6 @@ import type {
35
39
  SqlIndexIR,
36
40
  SqlReferentialAction,
37
41
  SqlSchemaIR,
38
- SqlTableIR,
39
42
  SqlUniqueIR,
40
43
  } from '@prisma-next/sql-schema-ir/types';
41
44
  import {
@@ -45,14 +48,31 @@ import {
45
48
  import type {
46
49
  AddColumnAction,
47
50
  AlterTableActionVisitor,
51
+ DropDefaultAction,
48
52
  PostgresAlterTable,
53
+ PostgresCreatePolicy,
49
54
  PostgresCreateSchema,
50
55
  PostgresCreateTable,
51
56
  PostgresDdlNode,
57
+ PostgresDropPolicy,
58
+ RlsPolicyOperation,
52
59
  } from '@prisma-next/target-postgres/ddl';
53
60
  import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
54
61
  import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
62
+ import {
63
+ contractToPostgresSchemaIR,
64
+ diffPostgresSchema,
65
+ filterIssuesByOwnership,
66
+ } from '@prisma-next/target-postgres/planner';
55
67
  import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
68
+ import {
69
+ ensurePostgresSchemaIR,
70
+ isPostgresSchemaIR,
71
+ PostgresRlsPolicy,
72
+ PostgresRole,
73
+ PostgresSchemaIR,
74
+ PostgresTableIR,
75
+ } from '@prisma-next/target-postgres/types';
56
76
  import { blindCast } from '@prisma-next/utils/casts';
57
77
  import { ifDefined } from '@prisma-next/utils/defined';
58
78
  import { encodeControlQueryParams } from './control-codecs';
@@ -108,6 +128,31 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
108
128
  */
109
129
  readonly normalizeNativeType = normalizeSchemaNativeType;
110
130
 
131
+ collectSchemaDiffIssues(
132
+ contract: Contract<SqlStorage>,
133
+ schema: SqlSchemaIR,
134
+ ): readonly SchemaDiffIssue[] {
135
+ if (!isPostgresSchemaIR(schema)) {
136
+ throw new Error(
137
+ `Postgres schema diff requires a PostgresSchemaIR; got ${(schema as { constructor?: { name?: string } }).constructor?.name ?? typeof schema}`,
138
+ );
139
+ }
140
+ const expected = contractToPostgresSchemaIR(
141
+ blindCast<
142
+ PostgresContract,
143
+ 'collectSchemaDiffIssues is only called with a postgres contract'
144
+ >(contract),
145
+ { annotationNamespace: 'pg' },
146
+ );
147
+ const actual = ensurePostgresSchemaIR(schema);
148
+ const issues = diffPostgresSchema(expected, actual);
149
+ const ownedSchemaNames = new Set([
150
+ ...expected.rlsPolicies.map((p) => p.namespaceId),
151
+ ...expected.existingSchemas,
152
+ ]);
153
+ return filterIssuesByOwnership(issues, ownedSchemaNames);
154
+ }
155
+
111
156
  bootstrapControlTableQueries(): readonly DdlNode[] {
112
157
  return buildControlTableBootstrapQueries();
113
158
  }
@@ -548,26 +593,21 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
548
593
  driver: SqlControlDriverInstance<'postgres'>,
549
594
  contract?: unknown,
550
595
  schema = 'public',
551
- ): Promise<SqlSchemaIR> {
596
+ ): Promise<PostgresSchemaIR> {
552
597
  const declaredNamespaces = extractContractNamespaceIds(contract);
553
598
  const ir =
554
599
  declaredNamespaces.length > 0
555
600
  ? await this.introspectNamespaces(driver, declaredNamespaces)
556
601
  : await this.introspectSchema(driver, schema);
557
- // Capture the list of non-system schemas so downstream planners
558
- // (e.g. `verifyPostgresNamespacePresence`) can determine which
559
- // contract-declared namespaces need a `CREATE SCHEMA` before the
560
- // table DDL.
561
602
  const existingSchemas = await this.listExistingSchemas(driver);
562
- const annotations = ir.annotations ?? {};
563
- const pg = (annotations as { pg?: Record<string, unknown> }).pg ?? {};
564
- return {
565
- ...ir,
566
- annotations: {
567
- ...annotations,
568
- pg: { ...pg, existingSchemas },
569
- },
570
- };
603
+ return new PostgresSchemaIR({
604
+ tables: ir.tables,
605
+ pgSchemaName: ir.pgSchemaName,
606
+ pgVersion: ir.pgVersion,
607
+ roles: ir.roles,
608
+ existingSchemas,
609
+ nativeEnumTypeNames: ir.nativeEnumTypeNames,
610
+ });
571
611
  }
572
612
 
573
613
  /**
@@ -602,7 +642,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
602
642
  private async introspectNamespaces(
603
643
  driver: SqlControlDriverInstance<'postgres'>,
604
644
  namespaceIds: readonly string[],
605
- ): Promise<SqlSchemaIR> {
645
+ ): Promise<PostgresSchemaIR> {
606
646
  const resolvedSchemas: string[] = [];
607
647
  for (const id of namespaceIds) {
608
648
  if (id === UNBOUND_NAMESPACE_ID) {
@@ -619,30 +659,29 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
619
659
  // Walk schemas sequentially: every introspectSchema call shares the one
620
660
  // control connection, so a parallel walk only serialises behind the wire
621
661
  // protocol and trips pg's "already executing a query" deprecation.
622
- const perSchema: SqlSchemaIR[] = [];
662
+ const perSchema: PostgresSchemaIR[] = [];
623
663
  for (const schema of uniqueSchemas) {
624
664
  perSchema.push(await this.introspectSchema(driver, schema));
625
665
  }
626
666
 
627
- const mergedTables: Record<string, SqlTableIR> = {};
667
+ const mergedTables: Record<string, PostgresTableIR> = {};
668
+ const mergedRoles: PostgresRole[] = [];
628
669
  for (const ir of perSchema) {
629
670
  for (const [tableName, table] of Object.entries(ir.tables)) {
630
671
  mergedTables[tableName] = table;
631
672
  }
673
+ mergedRoles.push(...ir.roles);
632
674
  }
633
675
 
634
- const firstAnnotations = perSchema[0]?.annotations;
635
- const firstPg =
636
- blindCast<Record<string, unknown> | undefined, 'pg annotation envelope index slot'>(
637
- firstAnnotations?.['pg'],
638
- ) ?? {};
639
- return {
676
+ const first = perSchema[0];
677
+ return new PostgresSchemaIR({
640
678
  tables: mergedTables,
641
- ...ifDefined('annotations', {
642
- ...firstAnnotations,
643
- pg: { ...firstPg },
644
- }),
645
- };
679
+ pgSchemaName: first?.pgSchemaName ?? 'public',
680
+ pgVersion: first?.pgVersion ?? 'unknown',
681
+ roles: mergedRoles,
682
+ existingSchemas: first?.existingSchemas ?? ['public'],
683
+ nativeEnumTypeNames: first?.nativeEnumTypeNames ?? [],
684
+ });
646
685
  }
647
686
 
648
687
  /**
@@ -653,7 +692,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
653
692
  private async introspectSchema(
654
693
  driver: SqlControlDriverInstance<'postgres'>,
655
694
  schema: string,
656
- ): Promise<SqlSchemaIR> {
695
+ ): Promise<PostgresSchemaIR> {
657
696
  // Issue the schema-wide queries one at a time. A single control connection
658
697
  // serialises queries anyway, so Promise.all buys no parallelism here and
659
698
  // makes pg emit a "client is already executing a query" deprecation. One
@@ -895,7 +934,18 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
895
934
  constraints.add(row.constraint_name);
896
935
  }
897
936
 
898
- const tables: Record<string, SqlTableIR> = {};
937
+ const tableInputs: Record<
938
+ string,
939
+ {
940
+ name: string;
941
+ columns: Record<string, SqlColumnIR>;
942
+ primaryKey?: PrimaryKey;
943
+ foreignKeys: readonly SqlForeignKeyIR[];
944
+ uniques: readonly SqlUniqueIR[];
945
+ indexes: readonly SqlIndexIR[];
946
+ checks?: SqlCheckConstraintIRInput[];
947
+ }
948
+ > = {};
899
949
 
900
950
  for (const tableRow of tablesResult.rows) {
901
951
  const tableName = tableRow.table_name;
@@ -927,11 +977,22 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
927
977
  nativeType = colRow.udt_name || colRow.data_type;
928
978
  }
929
979
 
980
+ // Postgres reports array columns as data_type='ARRAY'; the element type
981
+ // is the `nativeType` string minus the trailing `[]`. Strip the suffix,
982
+ // normalize the element type to the canonical form (e.g. `integer` →
983
+ // `int4`), and record `many: true` so introspection consumers (verifier,
984
+ // psl-contract-infer) can reconstruct the full array type as needed.
985
+ const many = nativeType.endsWith('[]') ? true : undefined;
986
+ if (many) {
987
+ nativeType = normalizeSchemaNativeType(nativeType.slice(0, -2));
988
+ }
989
+
930
990
  columns[colRow.column_name] = {
931
991
  name: colRow.column_name,
932
992
  nativeType,
933
993
  nullable: colRow.is_nullable === 'YES',
934
994
  ...ifDefined('default', colRow.column_default ?? undefined),
995
+ ...ifDefined('many', many),
935
996
  };
936
997
  }
937
998
 
@@ -1069,7 +1130,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1069
1130
  }
1070
1131
  }
1071
1132
 
1072
- tables[tableName] = {
1133
+ tableInputs[tableName] = {
1073
1134
  name: tableName,
1074
1135
  columns,
1075
1136
  ...ifDefined('primaryKey', primaryKey),
@@ -1090,19 +1151,74 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1090
1151
  [schema],
1091
1152
  );
1092
1153
  const nativeEnumTypeNames = nativeEnumResult.rows.map((r) => r.typname);
1154
+ const policiesResult = await driver.query<{
1155
+ schemaname: string;
1156
+ tablename: string;
1157
+ policyname: string;
1158
+ cmd: string;
1159
+ roles: string[];
1160
+ qual: string | null;
1161
+ with_check: string | null;
1162
+ permissive: string;
1163
+ }>(
1164
+ `SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
1165
+ FROM pg_catalog.pg_policies
1166
+ WHERE schemaname = $1
1167
+ ORDER BY tablename, policyname`,
1168
+ [schema],
1169
+ );
1170
+ const rolesResult = await driver.query<{ rolname: string }>(
1171
+ `SELECT rolname
1172
+ FROM pg_catalog.pg_roles
1173
+ WHERE rolname NOT LIKE 'pg_%'
1174
+ AND rolname != 'postgres'
1175
+ ORDER BY rolname`,
1176
+ );
1177
+ const policiesByTable = new Map<string, PostgresRlsPolicy[]>();
1178
+ for (const row of policiesResult.rows) {
1179
+ const operation = mapPgCmd(row.cmd);
1180
+ const policyRoles = [
1181
+ ...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
1182
+ ].sort();
1183
+ const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
1184
+ const hashSuffixMatch = /^(.+)_([0-9a-f]{8})$/.exec(row.policyname);
1185
+ const prefix = hashSuffixMatch?.[1] ?? row.policyname;
1186
+ const policy = new PostgresRlsPolicy({
1187
+ name: row.policyname,
1188
+ prefix,
1189
+ tableName: row.tablename,
1190
+ namespaceId: row.schemaname,
1191
+ operation,
1192
+ roles: policyRoles,
1193
+ ...(row.qual !== null ? { using: row.qual } : {}),
1194
+ ...(row.with_check !== null ? { withCheck: row.with_check } : {}),
1195
+ permissive,
1196
+ });
1197
+ const list = policiesByTable.get(row.tablename) ?? [];
1198
+ list.push(policy);
1199
+ policiesByTable.set(row.tablename, list);
1200
+ }
1201
+
1202
+ const tables: Record<string, PostgresTableIR> = {};
1203
+ for (const [tableName, input] of Object.entries(tableInputs)) {
1204
+ tables[tableName] = new PostgresTableIR({
1205
+ ...input,
1206
+ rlsPolicies: policiesByTable.get(tableName) ?? [],
1207
+ });
1208
+ }
1093
1209
 
1094
- const annotations = {
1095
- pg: {
1096
- schema,
1097
- version: await this.getPostgresVersion(driver),
1098
- ...(nativeEnumTypeNames.length > 0 && { nativeEnumTypeNames }),
1099
- },
1100
- };
1210
+ const roles: PostgresRole[] = rolesResult.rows.map(
1211
+ (row) => new PostgresRole({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
1212
+ );
1101
1213
 
1102
- return {
1214
+ return new PostgresSchemaIR({
1103
1215
  tables,
1104
- annotations,
1105
- };
1216
+ pgSchemaName: schema,
1217
+ pgVersion: await this.getPostgresVersion(driver),
1218
+ roles,
1219
+ existingSchemas: [],
1220
+ nativeEnumTypeNames,
1221
+ });
1106
1222
  }
1107
1223
 
1108
1224
  /**
@@ -1117,6 +1233,52 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1117
1233
  }
1118
1234
  }
1119
1235
 
1236
+ /**
1237
+ * Normalises a `name[]` column value from `pg_policies.roles`.
1238
+ *
1239
+ * The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
1240
+ * `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
1241
+ * literal (`{role1,role2}`) is returned as a string instead of a JS array.
1242
+ * This function accepts either form and returns a plain string array.
1243
+ */
1244
+ function parsePgNameArray(value: unknown): string[] {
1245
+ if (Array.isArray(value)) {
1246
+ return value as string[];
1247
+ }
1248
+ if (typeof value !== 'string') {
1249
+ return [];
1250
+ }
1251
+ const trimmed = value.trim();
1252
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
1253
+ return [];
1254
+ }
1255
+ const inner = trimmed.slice(1, -1);
1256
+ if (inner === '') {
1257
+ return [];
1258
+ }
1259
+ return inner.split(',').map((s) => s.trim());
1260
+ }
1261
+
1262
+ /**
1263
+ * Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
1264
+ * The `pg_policies` view renders the internal command code as an uppercase
1265
+ * English keyword; this function lowercases to match the IR type.
1266
+ */
1267
+ function mapPgCmd(cmd: string): RlsPolicyOperation {
1268
+ switch (cmd.toUpperCase()) {
1269
+ case 'SELECT':
1270
+ return 'select';
1271
+ case 'INSERT':
1272
+ return 'insert';
1273
+ case 'UPDATE':
1274
+ return 'update';
1275
+ case 'DELETE':
1276
+ return 'delete';
1277
+ default:
1278
+ return 'all';
1279
+ }
1280
+ }
1281
+
1120
1282
  /**
1121
1283
  * Extracts the namespace coordinate ids declared on a contract's storage,
1122
1284
  * or returns an empty array when no contract (or no storage / namespaces)
@@ -1133,6 +1295,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
1133
1295
  }
1134
1296
 
1135
1297
  function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
1298
+ if (formattedType.endsWith('[]')) {
1299
+ return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
1300
+ }
1136
1301
  if (formattedType === 'integer') {
1137
1302
  return 'int4';
1138
1303
  }
@@ -1376,6 +1541,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
1376
1541
  );
1377
1542
  }
1378
1543
 
1544
+ function pgRenderArrayElement(el: unknown): string {
1545
+ if (el === null) return 'NULL';
1546
+ if (typeof el === 'number' || typeof el === 'boolean') return String(el);
1547
+ if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
1548
+ return `'${escapeLiteral(JSON.stringify(el))}'`;
1549
+ }
1550
+
1551
+ function pgRenderArrayLiteral(elements: unknown[]): string {
1552
+ if (elements.length === 0) return "'{}'";
1553
+ return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
1554
+ }
1555
+
1379
1556
  function pgInlineLiteral(wire: unknown, nativeType: string): string {
1380
1557
  if (wire === null) return 'NULL';
1381
1558
  if (typeof wire === 'boolean') return wire ? 'true' : 'false';
@@ -1407,6 +1584,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
1407
1584
  .join('');
1408
1585
  return `'\\x${hex}'::${nativeType}`;
1409
1586
  }
1587
+ if (Array.isArray(wire) && nativeType.endsWith('[]')) {
1588
+ return pgRenderArrayLiteral(wire);
1589
+ }
1410
1590
  if (typeof wire === 'object') {
1411
1591
  const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
1412
1592
  return `${quoted}::${nativeType}`;
@@ -1524,6 +1704,9 @@ async function pgRenderAlterTable(
1524
1704
  const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
1525
1705
  return `ADD COLUMN ${colFragment}`;
1526
1706
  },
1707
+ dropDefault(action: DropDefaultAction): Promise<string> {
1708
+ return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
1709
+ },
1527
1710
  };
1528
1711
  const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
1529
1712
  return {
@@ -1532,6 +1715,37 @@ async function pgRenderAlterTable(
1532
1715
  };
1533
1716
  }
1534
1717
 
1718
+ const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
1719
+ select: 'SELECT',
1720
+ insert: 'INSERT',
1721
+ update: 'UPDATE',
1722
+ delete: 'DELETE',
1723
+ all: 'ALL',
1724
+ };
1725
+
1726
+ function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
1727
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1728
+ const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
1729
+ const command = POLICY_OPERATION_SQL[node.operation];
1730
+ const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
1731
+ let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
1732
+ if (node.using !== undefined) {
1733
+ sql += ` USING (${node.using})`;
1734
+ }
1735
+ if (node.withCheck !== undefined) {
1736
+ sql += ` WITH CHECK (${node.withCheck})`;
1737
+ }
1738
+ return { sql, params: [] };
1739
+ }
1740
+
1741
+ function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
1742
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1743
+ return {
1744
+ sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
1745
+ params: [],
1746
+ };
1747
+ }
1748
+
1535
1749
  async function pgRenderDdlExecuteRequest(
1536
1750
  ast: PostgresDdlNode,
1537
1751
  codecLookup: CodecLookup,
@@ -1540,6 +1754,8 @@ async function pgRenderDdlExecuteRequest(
1540
1754
  createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
1541
1755
  createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
1542
1756
  alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
1757
+ createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
1758
+ dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
1543
1759
  };
1544
1760
  return ast.accept(visitor);
1545
1761
  }
@@ -172,6 +172,7 @@ export const postgresAdapterDescriptorMeta = {
172
172
  returning: true,
173
173
  defaultInInsert: true,
174
174
  lateral: true,
175
+ scalarList: true,
175
176
  },
176
177
  },
177
178
  types: {
@@ -73,6 +73,7 @@ function renderTypedParam(
73
73
  index: number,
74
74
  codecId: string | undefined,
75
75
  codecLookup: CodecLookup,
76
+ many?: boolean,
76
77
  ): string {
77
78
  if (codecId === undefined) {
78
79
  return `$${index}`;
@@ -97,8 +98,14 @@ function renderTypedParam(
97
98
  const sqlBlock = isRecord(dbRecord) ? dbRecord['sql'] : undefined;
98
99
  const dialectBlock = isRecord(sqlBlock) ? sqlBlock['postgres'] : undefined;
99
100
  const nativeType = isRecord(dialectBlock) ? dialectBlock['nativeType'] : undefined;
100
- if (typeof nativeType === 'string' && !POSTGRES_INFERRABLE_NATIVE_TYPES.has(nativeType)) {
101
- return `$${index}::${nativeType}`;
101
+ if (typeof nativeType === 'string') {
102
+ const arraySuffix = many ? '[]' : '';
103
+ if (!POSTGRES_INFERRABLE_NATIVE_TYPES.has(nativeType)) {
104
+ return `$${index}::${nativeType}${arraySuffix}`;
105
+ }
106
+ if (many) {
107
+ return `$${index}::${nativeType}${arraySuffix}`;
108
+ }
102
109
  }
103
110
  return `$${index}`;
104
111
  }
@@ -736,7 +743,7 @@ function renderParamRef(ref: AnyParamRef, pim: ParamIndexMap): string {
736
743
  throw new Error('ParamRef not found in index map');
737
744
  }
738
745
  if (ref.kind === 'prepared-param-ref') {
739
- return renderTypedParam(index, ref.codec.codecId, pim.codecLookup);
746
+ return renderTypedParam(index, ref.codec.codecId, pim.codecLookup, ref.codec.many);
740
747
  }
741
748
  if (ref.codec === undefined) {
742
749
  throw runtimeError(
@@ -747,7 +754,7 @@ function renderParamRef(ref: AnyParamRef, pim: ParamIndexMap): string {
747
754
  { paramIndex: index, ...ifDefined('name', ref.name) },
748
755
  );
749
756
  }
750
- return renderTypedParam(index, ref.codec.codecId, pim.codecLookup);
757
+ return renderTypedParam(index, ref.codec.codecId, pim.codecLookup, ref.codec.many);
751
758
  }
752
759
 
753
760
  function renderLiteral(expr: LiteralExpr): string {
package/src/core/types.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { Contract } from '@prisma-next/contract/types';
2
1
  import type { CodecRegistry } from '@prisma-next/framework-components/codec';
3
- import type { SqlStorage, StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
2
+ import type { StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
4
3
  import type {
5
4
  AnyQueryAst,
6
5
  BinaryExpr,
@@ -32,7 +31,7 @@ export interface PostgresAdapterOptions {
32
31
  readonly codecLookup?: CodecRegistry;
33
32
  }
34
33
 
35
- export type PostgresContract = Contract<SqlStorage> & { readonly target: 'postgres' };
34
+ export type { PostgresContract } from '@prisma-next/target-postgres/types';
36
35
 
37
36
  export type Expr = ColumnRef | ParamRef | DefaultValueExpr;
38
37
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-CwkcdpM_.mjs","names":[],"sources":["../src/core/adapter.ts"],"sourcesContent":["import type { CodecRegistry } from '@prisma-next/framework-components/codec';\nimport { APP_SPACE_ID } from '@prisma-next/framework-components/control';\nimport type {\n Adapter,\n AdapterProfile,\n AnyQueryAst,\n LowererContext,\n RawSqlLiteral,\n SqlQueryable,\n} from '@prisma-next/sql-relational-core/ast';\nimport { isDdlNode } from '@prisma-next/sql-relational-core/ast';\nimport type { RawCodecInferer } from '@prisma-next/sql-relational-core/expression';\nimport type { PostgresDdlNode } from '@prisma-next/target-postgres/ddl';\nimport { createPostgresBuiltinCodecLookup } from './codec-lookup';\nimport { PostgresControlAdapter } from './control-adapter';\nimport { renderLoweredSql } from './sql-renderer';\nimport type { PostgresAdapterOptions, PostgresContract, PostgresLoweredStatement } from './types';\n\nconst defaultCapabilities = Object.freeze({\n postgres: {\n orderBy: true,\n limit: true,\n lateral: true,\n jsonAgg: true,\n returning: true,\n distinctOn: true,\n },\n sql: {\n enums: true,\n returning: true,\n defaultInInsert: true,\n lateral: true,\n },\n});\n\nclass PostgresAdapterImpl\n implements Adapter<AnyQueryAst, PostgresContract, PostgresLoweredStatement>\n{\n // These fields make the adapter instance structurally compatible with RuntimeAdapterInstance<'sql', 'postgres'> without introducing a runtime-plane dependency.\n readonly familyId = 'sql' as const;\n readonly targetId = 'postgres' as const;\n\n readonly profile: AdapterProfile<'postgres'>;\n private readonly codecLookup: CodecRegistry;\n\n constructor(options?: PostgresAdapterOptions) {\n this.codecLookup = options?.codecLookup ?? createPostgresBuiltinCodecLookup();\n const controlAdapter = new PostgresControlAdapter(this.codecLookup);\n this.profile = Object.freeze({\n id: options?.profileId ?? 'postgres/default@1',\n target: 'postgres',\n capabilities: defaultCapabilities,\n readMarker: (queryable: SqlQueryable) =>\n controlAdapter.readMarkerDiscriminated(\n {\n familyId: 'sql',\n targetId: 'postgres',\n query: async <Row = Record<string, unknown>>(\n sql: string,\n params?: readonly unknown[],\n ) => {\n const result = await queryable.query<Row>(sql, params);\n return { rows: [...result.rows] };\n },\n close: async () => {},\n },\n APP_SPACE_ID,\n ),\n });\n }\n\n lower(\n ast: AnyQueryAst | PostgresDdlNode,\n context: LowererContext<PostgresContract>,\n ): PostgresLoweredStatement {\n if (isDdlNode(ast)) {\n throw new Error(\n 'lower() does not lower DDL on the runtime adapter — DDL lowering is a control-plane concern handled by the control adapter.',\n );\n }\n return renderLoweredSql(ast, context.contract, this.codecLookup);\n }\n}\n\n/** Codec-id lookup for bare-literal interpolations used by `fns.raw` on a postgres client. Contributed as the descriptor's static `rawCodecInferer` slot. */\nexport const postgresRawCodecInferer: RawCodecInferer = {\n inferCodec(value: RawSqlLiteral): string {\n switch (typeof value) {\n case 'number':\n return Number.isSafeInteger(value) && value % 1 === 0 ? 'pg/int4' : 'pg/float8';\n case 'bigint':\n return 'pg/int8';\n case 'string':\n return 'pg/text';\n case 'boolean':\n return 'pg/bool';\n case 'object':\n if (value instanceof Uint8Array) return 'pg/bytea';\n }\n throw new Error(\n 'unsupported JS value type for raw-SQL interpolation: wrap this value in `param(...)` with an explicit codec',\n );\n },\n};\n\nexport function createPostgresAdapter(options?: PostgresAdapterOptions) {\n return Object.freeze(new PostgresAdapterImpl(options));\n}\n"],"mappings":";;;;AAkBA,MAAM,sBAAsB,OAAO,OAAO;CACxC,UAAU;EACR,SAAS;EACT,OAAO;EACP,SAAS;EACT,SAAS;EACT,WAAW;EACX,YAAY;CACd;CACA,KAAK;EACH,OAAO;EACP,WAAW;EACX,iBAAiB;EACjB,SAAS;CACX;AACF,CAAC;AAED,IAAM,sBAAN,MAEA;CAEE,WAAoB;CACpB,WAAoB;CAEpB;CACA;CAEA,YAAY,SAAkC;EAC5C,KAAK,cAAc,SAAS,eAAe,iCAAiC;EAC5E,MAAM,iBAAiB,IAAI,uBAAuB,KAAK,WAAW;EAClE,KAAK,UAAU,OAAO,OAAO;GAC3B,IAAI,SAAS,aAAa;GAC1B,QAAQ;GACR,cAAc;GACd,aAAa,cACX,eAAe,wBACb;IACE,UAAU;IACV,UAAU;IACV,OAAO,OACL,KACA,WACG;KAEH,OAAO,EAAE,MAAM,CAAC,IAAG,MADE,UAAU,MAAW,KAAK,MAAM,EAAA,CAC3B,IAAI,EAAE;IAClC;IACA,OAAO,YAAY,CAAC;GACtB,GACA,YACF;EACJ,CAAC;CACH;CAEA,MACE,KACA,SAC0B;EAC1B,IAAI,UAAU,GAAG,GACf,MAAM,IAAI,MACR,6HACF;EAEF,OAAO,iBAAiB,KAAK,QAAQ,UAAU,KAAK,WAAW;CACjE;AACF;;AAGA,MAAa,0BAA2C,EACtD,WAAW,OAA8B;CACvC,QAAQ,OAAO,OAAf;EACE,KAAK,UACH,OAAO,OAAO,cAAc,KAAK,KAAK,QAAQ,MAAM,IAAI,YAAY;EACtE,KAAK,UACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,UACH,IAAI,iBAAiB,YAAY,OAAO;CAC5C;CACA,MAAM,IAAI,MACR,6GACF;AACF,EACF;AAEA,SAAgB,sBAAsB,SAAkC;CACtE,OAAO,OAAO,OAAO,IAAI,oBAAoB,OAAO,CAAC;AACvD"}