@prisma-next/adapter-postgres 0.14.0-dev.6 → 0.14.0-dev.60

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.
@@ -28,15 +28,13 @@ import type {
28
28
  } from '@prisma-next/sql-relational-core/ast';
29
29
  import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
30
30
  import type {
31
- PrimaryKey,
31
+ PrimaryKeyInput,
32
32
  SqlCheckConstraintIRInput,
33
- SqlColumnIR,
34
- SqlForeignKeyIR,
35
- SqlIndexIR,
33
+ SqlColumnIRInput,
34
+ SqlForeignKeyIRInput,
35
+ SqlIndexIRInput,
36
36
  SqlReferentialAction,
37
- SqlSchemaIR,
38
- SqlTableIR,
39
- SqlUniqueIR,
37
+ SqlUniqueIRInput,
40
38
  } from '@prisma-next/sql-schema-ir/types';
41
39
  import {
42
40
  buildControlTableBootstrapQueries,
@@ -45,14 +43,28 @@ import {
45
43
  import type {
46
44
  AddColumnAction,
47
45
  AlterTableActionVisitor,
46
+ DropDefaultAction,
47
+ PostgresAlterPolicyRename,
48
48
  PostgresAlterTable,
49
+ PostgresCreatePolicy,
49
50
  PostgresCreateSchema,
50
51
  PostgresCreateTable,
51
52
  PostgresDdlNode,
53
+ PostgresDisableRowLevelSecurity,
54
+ PostgresDropPolicy,
55
+ RlsPolicyOperation,
52
56
  } from '@prisma-next/target-postgres/ddl';
53
57
  import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
54
58
  import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
59
+ import { parseRlsPolicyWireName } from '@prisma-next/target-postgres/rls-canonicalize';
55
60
  import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
61
+ import {
62
+ PostgresDatabaseSchemaNode,
63
+ PostgresNamespaceSchemaNode,
64
+ PostgresPolicySchemaNode,
65
+ PostgresRoleSchemaNode,
66
+ PostgresTableSchemaNode,
67
+ } from '@prisma-next/target-postgres/types';
56
68
  import { blindCast } from '@prisma-next/utils/casts';
57
69
  import { ifDefined } from '@prisma-next/utils/defined';
58
70
  import { encodeControlQueryParams } from './control-codecs';
@@ -548,26 +560,51 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
548
560
  driver: SqlControlDriverInstance<'postgres'>,
549
561
  contract?: unknown,
550
562
  schema = 'public',
551
- ): Promise<SqlSchemaIR> {
563
+ ): Promise<PostgresDatabaseSchemaNode> {
552
564
  const declaredNamespaces = extractContractNamespaceIds(contract);
553
- const ir =
565
+ const resolvedSchemas =
554
566
  declaredNamespaces.length > 0
555
- ? await this.introspectNamespaces(driver, declaredNamespaces)
556
- : 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.
567
+ ? await this.resolveNamespaceSchemas(driver, declaredNamespaces)
568
+ : [schema];
569
+
570
+ // Walk schemas sequentially: every introspectSchema call shares the one
571
+ // control connection, so a parallel walk only serialises behind the wire
572
+ // protocol and trips pg's "already executing a query" deprecation.
573
+ const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};
574
+ let pgVersion = 'unknown';
575
+ for (const resolved of resolvedSchemas) {
576
+ const { namespace, pgVersion: version } = await this.introspectSchema(driver, resolved);
577
+ namespaces[resolved] = namespace;
578
+ pgVersion = version;
579
+ }
580
+
581
+ const roles = await this.introspectRoles(driver);
561
582
  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
- };
583
+ return new PostgresDatabaseSchemaNode({
584
+ namespaces,
585
+ roles,
586
+ existingSchemas,
587
+ pgVersion,
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Reads cluster-scoped database roles. Roles are not schema-qualified, so
593
+ * this is queried once for the whole database rather than per namespace.
594
+ */
595
+ private async introspectRoles(
596
+ driver: SqlControlDriverInstance<'postgres'>,
597
+ ): Promise<readonly PostgresRoleSchemaNode[]> {
598
+ const rolesResult = await driver.query<{ rolname: string }>(
599
+ `SELECT rolname
600
+ FROM pg_catalog.pg_roles
601
+ WHERE rolname NOT LIKE 'pg_%'
602
+ AND rolname != 'postgres'
603
+ ORDER BY rolname`,
604
+ );
605
+ return rolesResult.rows.map(
606
+ (row) => new PostgresRoleSchemaNode({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
607
+ );
571
608
  }
572
609
 
573
610
  /**
@@ -593,16 +630,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
593
630
  }
594
631
 
595
632
  /**
596
- * Walks every declared namespace, resolving `UNBOUND_NAMESPACE_ID` to
597
- * the connection's `current_schema()`, and merges the per-schema results
598
- * into a single `SqlSchemaIR`. The merged `tables` map is flat (keyed by
599
- * table name) so callers that look up by `tableName` see every contract
600
- * table regardless of which namespace it lives in.
633
+ * Resolves the declared namespace ids to their live DDL schema names,
634
+ * mapping `UNBOUND_NAMESPACE_ID` to the connection's `current_schema()`
635
+ * and de-duplicating. The caller introspects one namespace node per
636
+ * resolved schema there is no flat cross-schema merge, so two schemas
637
+ * holding a same-named table no longer collide.
601
638
  */
602
- private async introspectNamespaces(
639
+ private async resolveNamespaceSchemas(
603
640
  driver: SqlControlDriverInstance<'postgres'>,
604
641
  namespaceIds: readonly string[],
605
- ): Promise<SqlSchemaIR> {
642
+ ): Promise<readonly string[]> {
606
643
  const resolvedSchemas: string[] = [];
607
644
  for (const id of namespaceIds) {
608
645
  if (id === UNBOUND_NAMESPACE_ID) {
@@ -614,46 +651,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
614
651
  resolvedSchemas.push(id);
615
652
  }
616
653
  }
617
- const uniqueSchemas = Array.from(new Set(resolvedSchemas));
618
-
619
- // Walk schemas sequentially: every introspectSchema call shares the one
620
- // control connection, so a parallel walk only serialises behind the wire
621
- // protocol and trips pg's "already executing a query" deprecation.
622
- const perSchema: SqlSchemaIR[] = [];
623
- for (const schema of uniqueSchemas) {
624
- perSchema.push(await this.introspectSchema(driver, schema));
625
- }
626
-
627
- const mergedTables: Record<string, SqlTableIR> = {};
628
- for (const ir of perSchema) {
629
- for (const [tableName, table] of Object.entries(ir.tables)) {
630
- mergedTables[tableName] = table;
631
- }
632
- }
633
-
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 {
640
- tables: mergedTables,
641
- ...ifDefined('annotations', {
642
- ...firstAnnotations,
643
- pg: { ...firstPg },
644
- }),
645
- };
654
+ return Array.from(new Set(resolvedSchemas));
646
655
  }
647
656
 
648
657
  /**
649
- * Introspects a single Postgres schema and returns a raw SqlSchemaIR
650
- * containing only the tables in that schema. Used by `introspect` as
658
+ * Introspects a single Postgres schema and returns the namespace node for
659
+ * that schema (its tables, their policies, and its native enum type names),
660
+ * alongside the cluster-scoped Postgres version. Used by `introspect` as
651
661
  * the per-namespace walk.
652
662
  */
653
663
  private async introspectSchema(
654
664
  driver: SqlControlDriverInstance<'postgres'>,
655
665
  schema: string,
656
- ): Promise<SqlSchemaIR> {
666
+ ): Promise<{ readonly namespace: PostgresNamespaceSchemaNode; readonly pgVersion: string }> {
657
667
  // Issue the schema-wide queries one at a time. A single control connection
658
668
  // serialises queries anyway, so Promise.all buys no parallelism here and
659
669
  // makes pg emit a "client is already executing a query" deprecation. One
@@ -895,13 +905,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
895
905
  constraints.add(row.constraint_name);
896
906
  }
897
907
 
898
- const tables: Record<string, SqlTableIR> = {};
908
+ const tableInputs: Record<
909
+ string,
910
+ {
911
+ name: string;
912
+ columns: Record<string, SqlColumnIRInput>;
913
+ primaryKey?: PrimaryKeyInput;
914
+ foreignKeys: readonly SqlForeignKeyIRInput[];
915
+ uniques: readonly SqlUniqueIRInput[];
916
+ indexes: readonly SqlIndexIRInput[];
917
+ checks?: SqlCheckConstraintIRInput[];
918
+ }
919
+ > = {};
899
920
 
900
921
  for (const tableRow of tablesResult.rows) {
901
922
  const tableName = tableRow.table_name;
902
923
 
903
924
  // Process columns for this table
904
- const columns: Record<string, SqlColumnIR> = {};
925
+ const columns: Record<string, SqlColumnIRInput> = {};
905
926
  for (const colRow of columnsByTable.get(tableName) ?? []) {
906
927
  let nativeType = colRow.udt_name;
907
928
  const formattedType = colRow.formatted_type
@@ -927,11 +948,36 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
927
948
  nativeType = colRow.udt_name || colRow.data_type;
928
949
  }
929
950
 
951
+ // Postgres reports array columns as data_type='ARRAY'; the element type
952
+ // is the `nativeType` string minus the trailing `[]`. Strip the suffix,
953
+ // normalize the element type to the canonical form (e.g. `integer` →
954
+ // `int4`), and record `many: true` so introspection consumers (verifier,
955
+ // psl-contract-infer) can reconstruct the full array type as needed.
956
+ const many = nativeType.endsWith('[]') ? true : undefined;
957
+ if (many) {
958
+ nativeType = normalizeSchemaNativeType(nativeType.slice(0, -2));
959
+ }
960
+
961
+ // Resolved values comparable against the contract-derived expected
962
+ // side: the normalized full native type (`[]` appended for arrays)
963
+ // and the structured parse of the raw default. Raw fields stay
964
+ // untouched alongside — the relational walk still reads and
965
+ // normalizes them itself.
966
+ const resolvedNativeType = `${normalizeSchemaNativeType(nativeType)}${many ? '[]' : ''}`;
967
+ const rawDefault = colRow.column_default ?? undefined;
930
968
  columns[colRow.column_name] = {
931
969
  name: colRow.column_name,
932
970
  nativeType,
933
971
  nullable: colRow.is_nullable === 'YES',
934
- ...ifDefined('default', colRow.column_default ?? undefined),
972
+ ...ifDefined('default', rawDefault),
973
+ ...ifDefined('many', many),
974
+ resolvedNativeType,
975
+ ...ifDefined(
976
+ 'resolvedDefault',
977
+ rawDefault !== undefined
978
+ ? parsePostgresDefault(rawDefault, resolvedNativeType)
979
+ : undefined,
980
+ ),
935
981
  };
936
982
  }
937
983
 
@@ -940,7 +986,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
940
986
  const primaryKeyColumns = pkRows
941
987
  .sort((a, b) => a.ordinal_position - b.ordinal_position)
942
988
  .map((row) => row.column_name);
943
- const primaryKey: PrimaryKey | undefined =
989
+ const primaryKey: PrimaryKeyInput | undefined =
944
990
  primaryKeyColumns.length > 0
945
991
  ? {
946
992
  columns: primaryKeyColumns,
@@ -978,7 +1024,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
978
1024
  });
979
1025
  }
980
1026
  }
981
- const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(foreignKeysMap.values()).map(
1027
+ const foreignKeys: readonly SqlForeignKeyIRInput[] = Array.from(foreignKeysMap.values()).map(
982
1028
  (fk) => ({
983
1029
  columns: Object.freeze([...fk.columns]) as readonly string[],
984
1030
  referencedTable: fk.referencedTable,
@@ -1008,7 +1054,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1008
1054
  });
1009
1055
  }
1010
1056
  }
1011
- const uniques: readonly SqlUniqueIR[] = Array.from(uniquesMap.values()).map((uq) => ({
1057
+ const uniques: readonly SqlUniqueIRInput[] = Array.from(uniquesMap.values()).map((uq) => ({
1012
1058
  columns: Object.freeze([...uq.columns]) as readonly string[],
1013
1059
  name: uq.name,
1014
1060
  }));
@@ -1046,7 +1092,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1046
1092
  });
1047
1093
  }
1048
1094
  }
1049
- const indexes: readonly SqlIndexIR[] = Array.from(indexesMap.values()).map((idx) => ({
1095
+ const indexes: readonly SqlIndexIRInput[] = Array.from(indexesMap.values()).map((idx) => ({
1050
1096
  columns: Object.freeze([...idx.columns]) as readonly string[],
1051
1097
  name: idx.name,
1052
1098
  unique: idx.unique,
@@ -1069,7 +1115,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1069
1115
  }
1070
1116
  }
1071
1117
 
1072
- tables[tableName] = {
1118
+ tableInputs[tableName] = {
1073
1119
  name: tableName,
1074
1120
  columns,
1075
1121
  ...ifDefined('primaryKey', primaryKey),
@@ -1080,29 +1126,108 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1080
1126
  };
1081
1127
  }
1082
1128
 
1083
- const nativeEnumResult = await driver.query<{ typname: string }>(
1084
- `SELECT t.typname
1129
+ const nativeEnumResult = await driver.query<{ typname: string; enumvalues: unknown }>(
1130
+ `SELECT t.typname, array_agg(e.enumlabel ORDER BY e.enumsortorder) AS enumvalues
1085
1131
  FROM pg_catalog.pg_type t
1086
1132
  JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
1133
+ JOIN pg_catalog.pg_enum e ON e.enumtypid = t.oid
1087
1134
  WHERE t.typtype = 'e'
1088
1135
  AND n.nspname = $1
1136
+ GROUP BY t.typname
1089
1137
  ORDER BY t.typname`,
1090
1138
  [schema],
1091
1139
  );
1092
- const nativeEnumTypeNames = nativeEnumResult.rows.map((r) => r.typname);
1140
+ const nativeEnums = nativeEnumResult.rows.map((r) => ({
1141
+ typeName: r.typname,
1142
+ values: parsePgNameArray(r.enumvalues),
1143
+ }));
1144
+ const nativeEnumTypeNames = nativeEnums.map((e) => e.typeName);
1145
+ const policiesResult = await driver.query<{
1146
+ schemaname: string;
1147
+ tablename: string;
1148
+ policyname: string;
1149
+ cmd: string;
1150
+ roles: string[];
1151
+ qual: string | null;
1152
+ with_check: string | null;
1153
+ permissive: string;
1154
+ }>(
1155
+ `SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
1156
+ FROM pg_catalog.pg_policies
1157
+ WHERE schemaname = $1
1158
+ ORDER BY tablename, policyname`,
1159
+ [schema],
1160
+ );
1161
+ const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();
1162
+ for (const row of policiesResult.rows) {
1163
+ const operation = mapPgCmd(row.cmd);
1164
+ const policyRoles = [
1165
+ ...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
1166
+ ].sort();
1167
+ const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
1168
+ const prefix = parseRlsPolicyWireName(row.policyname)?.prefix ?? row.policyname;
1169
+ const policy = new PostgresPolicySchemaNode({
1170
+ name: row.policyname,
1171
+ prefix,
1172
+ tableName: row.tablename,
1173
+ namespaceId: row.schemaname,
1174
+ operation,
1175
+ roles: policyRoles,
1176
+ ...(row.qual !== null ? { using: row.qual } : {}),
1177
+ ...(row.with_check !== null ? { withCheck: row.with_check } : {}),
1178
+ permissive,
1179
+ });
1180
+ const list = policiesByTable.get(row.tablename) ?? [];
1181
+ list.push(policy);
1182
+ policiesByTable.set(row.tablename, list);
1183
+ }
1093
1184
 
1094
- const annotations = {
1095
- pg: {
1096
- schema,
1097
- version: await this.getPostgresVersion(driver),
1098
- ...(nativeEnumTypeNames.length > 0 && { nativeEnumTypeNames }),
1099
- },
1100
- };
1185
+ // RLS enablement is a table attribute (`pg_class.relrowsecurity`), not a
1186
+ // function of the policy set — a table can have RLS on with zero
1187
+ // policies (deny-all) or policies present with RLS off. relkind covers
1188
+ // both plain ('r') and partitioned ('p') tables: the table listing above
1189
+ // (`information_schema.tables`, BASE TABLE) includes partitioned parents,
1190
+ // and Postgres supports RLS on them.
1191
+ //
1192
+ // Kept as a SEPARATE query from the table listing on purpose. Folding
1193
+ // relrowsecurity into the listing would mean replacing
1194
+ // `information_schema.tables` (which filters by the connection role's
1195
+ // grants) with a raw `pg_class` scan (which does not), changing WHICH
1196
+ // tables the introspection returns — a real behavior shift, not a
1197
+ // cleanup, and one the offline golden-diff can't catch (introspection is
1198
+ // live-only). The only cost of two queries is the concurrent-DDL window:
1199
+ // a table listed but missed by this scan defaults (`?? false`) to
1200
+ // RLS-off. That default is fail-safe — the worst case downstream is a
1201
+ // spurious ENABLE (idempotent), never a spurious DISABLE.
1202
+ const rlsEnabledResult = await driver.query<{ tablename: string; rls_enabled: boolean }>(
1203
+ `SELECT c.relname AS tablename, c.relrowsecurity AS rls_enabled
1204
+ FROM pg_catalog.pg_class c
1205
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
1206
+ WHERE n.nspname = $1
1207
+ AND c.relkind IN ('r', 'p')
1208
+ ORDER BY c.relname`,
1209
+ [schema],
1210
+ );
1211
+ const rlsEnabledByTable = new Map<string, boolean>(
1212
+ rlsEnabledResult.rows.map((row) => [row.tablename, row.rls_enabled]),
1213
+ );
1214
+
1215
+ const tables: Record<string, PostgresTableSchemaNode> = {};
1216
+ for (const [tableName, input] of Object.entries(tableInputs)) {
1217
+ tables[tableName] = new PostgresTableSchemaNode({
1218
+ ...input,
1219
+ policies: policiesByTable.get(tableName) ?? [],
1220
+ rlsEnabled: rlsEnabledByTable.get(tableName) ?? false,
1221
+ });
1222
+ }
1101
1223
 
1102
- return {
1224
+ const namespace = new PostgresNamespaceSchemaNode({
1225
+ schemaName: schema,
1103
1226
  tables,
1104
- annotations,
1105
- };
1227
+ nativeEnumTypeNames,
1228
+ nativeEnums,
1229
+ });
1230
+ return { namespace, pgVersion: await this.getPostgresVersion(driver) };
1106
1231
  }
1107
1232
 
1108
1233
  /**
@@ -1117,6 +1242,106 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1117
1242
  }
1118
1243
  }
1119
1244
 
1245
+ /**
1246
+ * Normalises a `name[]` column value from `pg_policies.roles`.
1247
+ *
1248
+ * The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
1249
+ * `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
1250
+ * literal (`{role1,role2}`) is returned as a string instead of a JS array.
1251
+ * This function accepts either form and returns a plain string array.
1252
+ *
1253
+ * The string branch honors Postgres array-literal quoting: an element
1254
+ * containing a comma, quote, backslash, brace, or significant whitespace is
1255
+ * emitted double-quoted with `\"` / `\\` escapes, and unquoted elements are
1256
+ * whitespace-trimmed — so a label like `in progress` or `say "hi"` parses to
1257
+ * its true value instead of being split or kept escaped.
1258
+ */
1259
+ export function parsePgNameArray(value: unknown): string[] {
1260
+ if (Array.isArray(value)) {
1261
+ return value.map(String);
1262
+ }
1263
+ if (typeof value !== 'string') {
1264
+ return [];
1265
+ }
1266
+ const trimmed = value.trim();
1267
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
1268
+ return [];
1269
+ }
1270
+ const inner = trimmed.slice(1, -1);
1271
+ if (inner === '') {
1272
+ return [];
1273
+ }
1274
+
1275
+ const elements: string[] = [];
1276
+ let current = '';
1277
+ let inQuotes = false;
1278
+ let wasQuoted = false;
1279
+ const pushCurrent = () => {
1280
+ elements.push(wasQuoted ? current : current.trim());
1281
+ current = '';
1282
+ wasQuoted = false;
1283
+ };
1284
+ let i = 0;
1285
+ while (i < inner.length) {
1286
+ const char = inner.charAt(i);
1287
+ if (inQuotes) {
1288
+ if (char === '\\') {
1289
+ current += inner[i + 1] ?? '';
1290
+ i += 2;
1291
+ continue;
1292
+ }
1293
+ if (char === '"') {
1294
+ inQuotes = false;
1295
+ i++;
1296
+ continue;
1297
+ }
1298
+ current += char;
1299
+ i++;
1300
+ continue;
1301
+ }
1302
+ if (char === '"') {
1303
+ inQuotes = true;
1304
+ wasQuoted = true;
1305
+ i++;
1306
+ continue;
1307
+ }
1308
+ if (char === ',') {
1309
+ pushCurrent();
1310
+ i++;
1311
+ continue;
1312
+ }
1313
+ current += char;
1314
+ i++;
1315
+ }
1316
+ // A still-open quote means the literal was malformed (e.g. `{"unterminated}`);
1317
+ // reject rather than emit the partial value.
1318
+ if (inQuotes) {
1319
+ return [];
1320
+ }
1321
+ pushCurrent();
1322
+ return elements;
1323
+ }
1324
+
1325
+ /**
1326
+ * Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
1327
+ * The `pg_policies` view renders the internal command code as an uppercase
1328
+ * English keyword; this function lowercases to match the IR type.
1329
+ */
1330
+ function mapPgCmd(cmd: string): RlsPolicyOperation {
1331
+ switch (cmd.toUpperCase()) {
1332
+ case 'SELECT':
1333
+ return 'select';
1334
+ case 'INSERT':
1335
+ return 'insert';
1336
+ case 'UPDATE':
1337
+ return 'update';
1338
+ case 'DELETE':
1339
+ return 'delete';
1340
+ default:
1341
+ return 'all';
1342
+ }
1343
+ }
1344
+
1120
1345
  /**
1121
1346
  * Extracts the namespace coordinate ids declared on a contract's storage,
1122
1347
  * or returns an empty array when no contract (or no storage / namespaces)
@@ -1133,6 +1358,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
1133
1358
  }
1134
1359
 
1135
1360
  function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
1361
+ if (formattedType.endsWith('[]')) {
1362
+ return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
1363
+ }
1136
1364
  if (formattedType === 'integer') {
1137
1365
  return 'int4';
1138
1366
  }
@@ -1376,6 +1604,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
1376
1604
  );
1377
1605
  }
1378
1606
 
1607
+ function pgRenderArrayElement(el: unknown): string {
1608
+ if (el === null) return 'NULL';
1609
+ if (typeof el === 'number' || typeof el === 'boolean') return String(el);
1610
+ if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
1611
+ return `'${escapeLiteral(JSON.stringify(el))}'`;
1612
+ }
1613
+
1614
+ function pgRenderArrayLiteral(elements: unknown[]): string {
1615
+ if (elements.length === 0) return "'{}'";
1616
+ return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
1617
+ }
1618
+
1379
1619
  function pgInlineLiteral(wire: unknown, nativeType: string): string {
1380
1620
  if (wire === null) return 'NULL';
1381
1621
  if (typeof wire === 'boolean') return wire ? 'true' : 'false';
@@ -1407,6 +1647,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
1407
1647
  .join('');
1408
1648
  return `'\\x${hex}'::${nativeType}`;
1409
1649
  }
1650
+ if (Array.isArray(wire) && nativeType.endsWith('[]')) {
1651
+ return pgRenderArrayLiteral(wire);
1652
+ }
1410
1653
  if (typeof wire === 'object') {
1411
1654
  const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
1412
1655
  return `${quoted}::${nativeType}`;
@@ -1477,6 +1720,9 @@ function pgRenderDdlConstraint(constraint: DdlTableConstraint): string {
1477
1720
  }
1478
1721
  return sql;
1479
1722
  }
1723
+ if (constraint.kind === 'check-expression') {
1724
+ return `CONSTRAINT ${quoteIdentifier(constraint.name)} CHECK (${constraint.expression})`;
1725
+ }
1480
1726
  const cols = constraint.columns.map(quoteIdentifier).join(', ');
1481
1727
  if (constraint.name !== undefined) {
1482
1728
  return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
@@ -1524,6 +1770,9 @@ async function pgRenderAlterTable(
1524
1770
  const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
1525
1771
  return `ADD COLUMN ${colFragment}`;
1526
1772
  },
1773
+ dropDefault(action: DropDefaultAction): Promise<string> {
1774
+ return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
1775
+ },
1527
1776
  };
1528
1777
  const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
1529
1778
  return {
@@ -1532,6 +1781,53 @@ async function pgRenderAlterTable(
1532
1781
  };
1533
1782
  }
1534
1783
 
1784
+ const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
1785
+ select: 'SELECT',
1786
+ insert: 'INSERT',
1787
+ update: 'UPDATE',
1788
+ delete: 'DELETE',
1789
+ all: 'ALL',
1790
+ };
1791
+
1792
+ function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
1793
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1794
+ const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
1795
+ const command = POLICY_OPERATION_SQL[node.operation];
1796
+ const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
1797
+ let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
1798
+ if (node.using !== undefined) {
1799
+ sql += ` USING (${node.using})`;
1800
+ }
1801
+ if (node.withCheck !== undefined) {
1802
+ sql += ` WITH CHECK (${node.withCheck})`;
1803
+ }
1804
+ return { sql, params: [] };
1805
+ }
1806
+
1807
+ function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
1808
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1809
+ return {
1810
+ sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
1811
+ params: [],
1812
+ };
1813
+ }
1814
+
1815
+ function pgRenderAlterPolicyRename(node: PostgresAlterPolicyRename): SqlExecuteRequest {
1816
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1817
+ return {
1818
+ sql: `ALTER POLICY ${quoteIdentifier(node.name)} ON ${tableRef} RENAME TO ${quoteIdentifier(node.newName)}`,
1819
+ params: [],
1820
+ };
1821
+ }
1822
+
1823
+ function pgRenderDisableRowLevelSecurity(node: PostgresDisableRowLevelSecurity): SqlExecuteRequest {
1824
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1825
+ return {
1826
+ sql: `ALTER TABLE ${tableRef} DISABLE ROW LEVEL SECURITY`,
1827
+ params: [],
1828
+ };
1829
+ }
1830
+
1535
1831
  async function pgRenderDdlExecuteRequest(
1536
1832
  ast: PostgresDdlNode,
1537
1833
  codecLookup: CodecLookup,
@@ -1540,6 +1836,12 @@ async function pgRenderDdlExecuteRequest(
1540
1836
  createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
1541
1837
  createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
1542
1838
  alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
1839
+ createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
1840
+ dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
1841
+ alterPolicyRename: (node: PostgresAlterPolicyRename) =>
1842
+ Promise.resolve(pgRenderAlterPolicyRename(node)),
1843
+ disableRowLevelSecurity: (node: PostgresDisableRowLevelSecurity) =>
1844
+ Promise.resolve(pgRenderDisableRowLevelSecurity(node)),
1543
1845
  };
1544
1846
  return ast.accept(visitor);
1545
1847
  }
@@ -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: {