@prisma-next/adapter-postgres 0.14.0 → 0.15.0-dev.10

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.
@@ -7,7 +7,7 @@ import {
7
7
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
8
8
  import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
9
9
  import type { CodecLookup, CodecRegistry } from '@prisma-next/framework-components/codec';
10
- import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
10
+ import { APP_SPACE_ID, type SchemaNodeRef } from '@prisma-next/framework-components/control';
11
11
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
12
12
  import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
13
13
  import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
@@ -28,16 +28,15 @@ 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';
39
+ import { RelationalSchemaNodeKind } from '@prisma-next/sql-schema-ir/types';
41
40
  import {
42
41
  buildControlTableBootstrapQueries,
43
42
  buildSignMarkerBootstrapQueries,
@@ -45,14 +44,32 @@ import {
45
44
  import type {
46
45
  AddColumnAction,
47
46
  AlterTableActionVisitor,
47
+ DropDefaultAction,
48
+ PostgresAlterPolicyRename,
48
49
  PostgresAlterTable,
50
+ PostgresCreatePolicy,
49
51
  PostgresCreateSchema,
50
52
  PostgresCreateTable,
53
+ PostgresCreateType,
51
54
  PostgresDdlNode,
55
+ PostgresDisableRowLevelSecurity,
56
+ PostgresDropPolicy,
57
+ PostgresDropType,
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 { parseRlsPolicyWireName } from '@prisma-next/target-postgres/rls-canonicalize';
55
63
  import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
64
+ import {
65
+ PostgresDatabaseSchemaNode,
66
+ PostgresNamespaceSchemaNode,
67
+ PostgresNativeEnumSchemaNode,
68
+ PostgresPolicySchemaNode,
69
+ PostgresRoleSchemaNode,
70
+ PostgresSchemaNodeKind,
71
+ PostgresTableSchemaNode,
72
+ } from '@prisma-next/target-postgres/types';
56
73
  import { blindCast } from '@prisma-next/utils/casts';
57
74
  import { ifDefined } from '@prisma-next/utils/defined';
58
75
  import { encodeControlQueryParams } from './control-codecs';
@@ -60,6 +77,7 @@ import {
60
77
  execute,
61
78
  infoSchemaTables,
62
79
  ledger,
80
+ ledgerContract,
63
81
  ledgerReadShape,
64
82
  marker,
65
83
  mergeInvariants,
@@ -429,7 +447,11 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
429
447
  }
430
448
 
431
449
  /**
432
- * Appends a ledger entry for `space`. See the
450
+ * Appends a ledger entry for `space`. When the edge carries a
451
+ * destination contract snapshot, the content-addressed
452
+ * `prisma_contract.contract` store is populated first (keyed by the
453
+ * destination hash, DO NOTHING on revisit) so a reader never sees a
454
+ * ledger row whose stored destination contract is missing. See the
433
455
  * `SqlControlAdapter.writeLedgerEntry` contract.
434
456
  */
435
457
  async writeLedgerEntry(
@@ -442,10 +464,23 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
442
464
  readonly migrationName: string;
443
465
  readonly migrationHash: string;
444
466
  readonly operations: readonly unknown[];
467
+ readonly destinationContractJson?: unknown;
445
468
  },
446
469
  ): Promise<void> {
470
+ const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
471
+ if (entry.destinationContractJson !== undefined) {
472
+ await execute(
473
+ lower,
474
+ driver,
475
+ ledgerContract
476
+ .upsert({ core_hash: entry.to, contract_json: entry.destinationContractJson })
477
+ .onConflict(ledgerContract.core_hash)
478
+ .doNothing()
479
+ .build(),
480
+ );
481
+ }
447
482
  await execute(
448
- (query) => this.lower(query, { contract: undefined }),
483
+ lower,
449
484
  driver,
450
485
  ledger
451
486
  .insert({
@@ -548,26 +583,51 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
548
583
  driver: SqlControlDriverInstance<'postgres'>,
549
584
  contract?: unknown,
550
585
  schema = 'public',
551
- ): Promise<SqlSchemaIR> {
586
+ ): Promise<PostgresDatabaseSchemaNode> {
552
587
  const declaredNamespaces = extractContractNamespaceIds(contract);
553
- const ir =
588
+ const resolvedSchemas =
554
589
  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.
590
+ ? await this.resolveNamespaceSchemas(driver, declaredNamespaces)
591
+ : [schema];
592
+
593
+ // Walk schemas sequentially: every introspectSchema call shares the one
594
+ // control connection, so a parallel walk only serialises behind the wire
595
+ // protocol and trips pg's "already executing a query" deprecation.
596
+ const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};
597
+ let pgVersion = 'unknown';
598
+ for (const resolved of resolvedSchemas) {
599
+ const { namespace, pgVersion: version } = await this.introspectSchema(driver, resolved);
600
+ namespaces[resolved] = namespace;
601
+ pgVersion = version;
602
+ }
603
+
604
+ const roles = await this.introspectRoles(driver);
561
605
  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
- };
606
+ return new PostgresDatabaseSchemaNode({
607
+ namespaces,
608
+ roles,
609
+ existingSchemas,
610
+ pgVersion,
611
+ });
612
+ }
613
+
614
+ /**
615
+ * Reads cluster-scoped database roles. Roles are not schema-qualified, so
616
+ * this is queried once for the whole database rather than per namespace.
617
+ */
618
+ private async introspectRoles(
619
+ driver: SqlControlDriverInstance<'postgres'>,
620
+ ): Promise<readonly PostgresRoleSchemaNode[]> {
621
+ const rolesResult = await driver.query<{ rolname: string }>(
622
+ `SELECT rolname
623
+ FROM pg_catalog.pg_roles
624
+ WHERE rolname NOT LIKE 'pg_%'
625
+ AND rolname != 'postgres'
626
+ ORDER BY rolname`,
627
+ );
628
+ return rolesResult.rows.map(
629
+ (row) => new PostgresRoleSchemaNode({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
630
+ );
571
631
  }
572
632
 
573
633
  /**
@@ -593,16 +653,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
593
653
  }
594
654
 
595
655
  /**
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.
656
+ * Resolves the declared namespace ids to their live DDL schema names,
657
+ * mapping `UNBOUND_NAMESPACE_ID` to the connection's `current_schema()`
658
+ * and de-duplicating. The caller introspects one namespace node per
659
+ * resolved schema there is no flat cross-schema merge, so two schemas
660
+ * holding a same-named table no longer collide.
601
661
  */
602
- private async introspectNamespaces(
662
+ private async resolveNamespaceSchemas(
603
663
  driver: SqlControlDriverInstance<'postgres'>,
604
664
  namespaceIds: readonly string[],
605
- ): Promise<SqlSchemaIR> {
665
+ ): Promise<readonly string[]> {
606
666
  const resolvedSchemas: string[] = [];
607
667
  for (const id of namespaceIds) {
608
668
  if (id === UNBOUND_NAMESPACE_ID) {
@@ -614,46 +674,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
614
674
  resolvedSchemas.push(id);
615
675
  }
616
676
  }
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
- };
677
+ return Array.from(new Set(resolvedSchemas));
646
678
  }
647
679
 
648
680
  /**
649
- * Introspects a single Postgres schema and returns a raw SqlSchemaIR
650
- * containing only the tables in that schema. Used by `introspect` as
681
+ * Introspects a single Postgres schema and returns the namespace node for
682
+ * that schema (its tables, their policies, and its native enum type names),
683
+ * alongside the cluster-scoped Postgres version. Used by `introspect` as
651
684
  * the per-namespace walk.
652
685
  */
653
686
  private async introspectSchema(
654
687
  driver: SqlControlDriverInstance<'postgres'>,
655
688
  schema: string,
656
- ): Promise<SqlSchemaIR> {
689
+ ): Promise<{ readonly namespace: PostgresNamespaceSchemaNode; readonly pgVersion: string }> {
657
690
  // Issue the schema-wide queries one at a time. A single control connection
658
691
  // serialises queries anyway, so Promise.all buys no parallelism here and
659
692
  // makes pg emit a "client is already executing a query" deprecation. One
@@ -706,7 +739,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
706
739
  ORDER BY c.table_name, c.ordinal_position`,
707
740
  [schema],
708
741
  );
709
- // Query all primary keys for all tables in schema
742
+ // Query all primary keys for all tables in schema. Reads pg_catalog, not
743
+ // information_schema: `information_schema.table_constraints` hides
744
+ // constraints on tables where the connecting role's only privilege is
745
+ // SELECT (its filter grants visibility for INSERT/UPDATE/DELETE/TRUNCATE/
746
+ // REFERENCES/TRIGGER — not SELECT), so a SELECT-only table introspects
747
+ // with all its columns but zero constraints and verify reports every
748
+ // declared constraint as missing. pg_catalog is not privilege-filtered.
710
749
  const pkResult = await driver.query<{
711
750
  table_name: string;
712
751
  constraint_name: string;
@@ -714,24 +753,29 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
714
753
  ordinal_position: number;
715
754
  }>(
716
755
  `SELECT
717
- tc.table_name,
718
- tc.constraint_name,
719
- kcu.column_name,
720
- kcu.ordinal_position
721
- FROM information_schema.table_constraints tc
722
- JOIN information_schema.key_column_usage kcu
723
- ON tc.constraint_name = kcu.constraint_name
724
- AND tc.table_schema = kcu.table_schema
725
- AND tc.table_name = kcu.table_name
726
- WHERE tc.table_schema = $1
727
- AND tc.constraint_type = 'PRIMARY KEY'
728
- ORDER BY tc.table_name, kcu.ordinal_position`,
756
+ cl.relname AS table_name,
757
+ con.conname AS constraint_name,
758
+ a.attname AS column_name,
759
+ k.ord AS ordinal_position
760
+ FROM pg_catalog.pg_constraint con
761
+ JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
762
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
763
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
764
+ JOIN pg_catalog.pg_attribute a
765
+ ON a.attrelid = con.conrelid
766
+ AND a.attnum = k.attnum
767
+ WHERE ns.nspname = $1
768
+ AND con.contype = 'p'
769
+ ORDER BY cl.relname, k.ord`,
729
770
  [schema],
730
771
  );
731
- // Query all foreign keys for all tables in schema, including referential actions.
732
- // Uses pg_catalog for correct positional pairing of composite FK columns
733
- // (information_schema.constraint_column_usage lacks ordinal_position,
734
- // which causes Cartesian products for multi-column FKs).
772
+ // Query all foreign keys for all tables in schema, including referential
773
+ // actions. Reads pg_catalog only (see the primary-key query above for why
774
+ // information_schema is unusable here); `unnest(conkey) WITH ORDINALITY`
775
+ // pairs source and referenced columns positionally, so composite FKs
776
+ // round-trip in declared order. The referential-action CASEs mirror
777
+ // pg_constraint's confdeltype/confupdtype encoding and produce the same
778
+ // rule strings information_schema.referential_constraints reports.
735
779
  const fkResult = await driver.query<{
736
780
  table_name: string;
737
781
  constraint_name: string;
@@ -744,41 +788,48 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
744
788
  update_rule: string;
745
789
  }>(
746
790
  `SELECT
747
- tc.table_name,
748
- tc.constraint_name,
749
- kcu.column_name,
750
- kcu.ordinal_position,
791
+ cl.relname AS table_name,
792
+ con.conname AS constraint_name,
793
+ a.attname AS column_name,
794
+ k.ord AS ordinal_position,
751
795
  ref_ns.nspname AS referenced_table_schema,
752
796
  ref_cl.relname AS referenced_table_name,
753
797
  ref_att.attname AS referenced_column_name,
754
- rc.delete_rule,
755
- rc.update_rule
756
- FROM information_schema.table_constraints tc
757
- JOIN information_schema.key_column_usage kcu
758
- ON tc.constraint_name = kcu.constraint_name
759
- AND tc.table_schema = kcu.table_schema
760
- AND tc.table_name = kcu.table_name
761
- JOIN pg_catalog.pg_constraint pgc
762
- ON pgc.conname = tc.constraint_name
763
- AND pgc.connamespace = (
764
- SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = tc.table_schema
765
- )
798
+ CASE con.confdeltype
799
+ WHEN 'a' THEN 'NO ACTION'
800
+ WHEN 'r' THEN 'RESTRICT'
801
+ WHEN 'c' THEN 'CASCADE'
802
+ WHEN 'n' THEN 'SET NULL'
803
+ WHEN 'd' THEN 'SET DEFAULT'
804
+ END AS delete_rule,
805
+ CASE con.confupdtype
806
+ WHEN 'a' THEN 'NO ACTION'
807
+ WHEN 'r' THEN 'RESTRICT'
808
+ WHEN 'c' THEN 'CASCADE'
809
+ WHEN 'n' THEN 'SET NULL'
810
+ WHEN 'd' THEN 'SET DEFAULT'
811
+ END AS update_rule
812
+ FROM pg_catalog.pg_constraint con
813
+ JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
814
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
815
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
816
+ JOIN pg_catalog.pg_attribute a
817
+ ON a.attrelid = con.conrelid
818
+ AND a.attnum = k.attnum
766
819
  JOIN pg_catalog.pg_class ref_cl
767
- ON ref_cl.oid = pgc.confrelid
820
+ ON ref_cl.oid = con.confrelid
768
821
  JOIN pg_catalog.pg_namespace ref_ns
769
822
  ON ref_ns.oid = ref_cl.relnamespace
770
823
  JOIN pg_catalog.pg_attribute ref_att
771
- ON ref_att.attrelid = pgc.confrelid
772
- AND ref_att.attnum = pgc.confkey[kcu.ordinal_position]
773
- JOIN information_schema.referential_constraints rc
774
- ON rc.constraint_name = tc.constraint_name
775
- AND rc.constraint_schema = tc.table_schema
776
- WHERE tc.table_schema = $1
777
- AND tc.constraint_type = 'FOREIGN KEY'
778
- ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
824
+ ON ref_att.attrelid = con.confrelid
825
+ AND ref_att.attnum = con.confkey[k.ord]
826
+ WHERE ns.nspname = $1
827
+ AND con.contype = 'f'
828
+ ORDER BY cl.relname, con.conname, k.ord`,
779
829
  [schema],
780
830
  );
781
- // Query all unique constraints for all tables in schema (excluding PKs)
831
+ // Query all unique constraints for all tables in schema (excluding PKs).
832
+ // Reads pg_catalog (see the primary-key query above).
782
833
  const uniqueResult = await driver.query<{
783
834
  table_name: string;
784
835
  constraint_name: string;
@@ -786,18 +837,20 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
786
837
  ordinal_position: number;
787
838
  }>(
788
839
  `SELECT
789
- tc.table_name,
790
- tc.constraint_name,
791
- kcu.column_name,
792
- kcu.ordinal_position
793
- FROM information_schema.table_constraints tc
794
- JOIN information_schema.key_column_usage kcu
795
- ON tc.constraint_name = kcu.constraint_name
796
- AND tc.table_schema = kcu.table_schema
797
- AND tc.table_name = kcu.table_name
798
- WHERE tc.table_schema = $1
799
- AND tc.constraint_type = 'UNIQUE'
800
- ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
840
+ cl.relname AS table_name,
841
+ con.conname AS constraint_name,
842
+ a.attname AS column_name,
843
+ k.ord AS ordinal_position
844
+ FROM pg_catalog.pg_constraint con
845
+ JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid
846
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
847
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
848
+ JOIN pg_catalog.pg_attribute a
849
+ ON a.attrelid = con.conrelid
850
+ AND a.attnum = k.attnum
851
+ WHERE ns.nspname = $1
852
+ AND con.contype = 'u'
853
+ ORDER BY cl.relname, con.conname, k.ord`,
801
854
  [schema],
802
855
  );
803
856
  // Query all indexes for all tables in schema (excluding constraints).
@@ -842,10 +895,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
842
895
  WHERE i.schemaname = $1
843
896
  AND NOT EXISTS (
844
897
  SELECT 1
845
- FROM information_schema.table_constraints tc
846
- WHERE tc.table_schema = $1
847
- AND tc.table_name = i.tablename
848
- AND tc.constraint_name = i.indexname
898
+ FROM pg_catalog.pg_constraint con
899
+ WHERE con.conindid = ic.oid
900
+ AND con.contype IN ('p', 'u', 'x')
849
901
  )
850
902
  ORDER BY i.tablename, i.indexname, k.ord`,
851
903
  [schema],
@@ -895,13 +947,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
895
947
  constraints.add(row.constraint_name);
896
948
  }
897
949
 
898
- const tables: Record<string, SqlTableIR> = {};
950
+ const tableInputs: Record<
951
+ string,
952
+ {
953
+ name: string;
954
+ columns: Record<string, SqlColumnIRInput>;
955
+ primaryKey?: PrimaryKeyInput;
956
+ foreignKeys: readonly SqlForeignKeyIRInput[];
957
+ uniques: readonly SqlUniqueIRInput[];
958
+ indexes: readonly SqlIndexIRInput[];
959
+ checks?: SqlCheckConstraintIRInput[];
960
+ }
961
+ > = {};
899
962
 
900
963
  for (const tableRow of tablesResult.rows) {
901
964
  const tableName = tableRow.table_name;
902
965
 
903
966
  // Process columns for this table
904
- const columns: Record<string, SqlColumnIR> = {};
967
+ const columns: Record<string, SqlColumnIRInput> = {};
905
968
  for (const colRow of columnsByTable.get(tableName) ?? []) {
906
969
  let nativeType = colRow.udt_name;
907
970
  const formattedType = colRow.formatted_type
@@ -927,11 +990,36 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
927
990
  nativeType = colRow.udt_name || colRow.data_type;
928
991
  }
929
992
 
993
+ // Postgres reports array columns as data_type='ARRAY'; the element type
994
+ // is the `nativeType` string minus the trailing `[]`. Strip the suffix,
995
+ // normalize the element type to the canonical form (e.g. `integer` →
996
+ // `int4`), and record `many: true` so introspection consumers (verifier,
997
+ // psl-contract-infer) can reconstruct the full array type as needed.
998
+ const many = nativeType.endsWith('[]') ? true : undefined;
999
+ if (many) {
1000
+ nativeType = normalizeSchemaNativeType(nativeType.slice(0, -2));
1001
+ }
1002
+
1003
+ // Resolved values comparable against the contract-derived expected
1004
+ // side: the normalized full native type (`[]` appended for arrays)
1005
+ // and the structured parse of the raw default. Raw fields stay
1006
+ // untouched alongside — the relational walk still reads and
1007
+ // normalizes them itself.
1008
+ const resolvedNativeType = `${normalizeSchemaNativeType(nativeType)}${many ? '[]' : ''}`;
1009
+ const rawDefault = colRow.column_default ?? undefined;
930
1010
  columns[colRow.column_name] = {
931
1011
  name: colRow.column_name,
932
1012
  nativeType,
933
1013
  nullable: colRow.is_nullable === 'YES',
934
- ...ifDefined('default', colRow.column_default ?? undefined),
1014
+ ...ifDefined('default', rawDefault),
1015
+ ...ifDefined('many', many),
1016
+ resolvedNativeType,
1017
+ ...ifDefined(
1018
+ 'resolvedDefault',
1019
+ rawDefault !== undefined
1020
+ ? parsePostgresDefault(rawDefault, resolvedNativeType)
1021
+ : undefined,
1022
+ ),
935
1023
  };
936
1024
  }
937
1025
 
@@ -940,11 +1028,12 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
940
1028
  const primaryKeyColumns = pkRows
941
1029
  .sort((a, b) => a.ordinal_position - b.ordinal_position)
942
1030
  .map((row) => row.column_name);
943
- const primaryKey: PrimaryKey | undefined =
1031
+ const primaryKey: PrimaryKeyInput | undefined =
944
1032
  primaryKeyColumns.length > 0
945
1033
  ? {
946
1034
  columns: primaryKeyColumns,
947
1035
  ...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
1036
+ dependsOn: postgresColumnDependsOn(schema, tableName, primaryKeyColumns),
948
1037
  }
949
1038
  : undefined;
950
1039
 
@@ -978,7 +1067,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
978
1067
  });
979
1068
  }
980
1069
  }
981
- const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(foreignKeysMap.values()).map(
1070
+ const foreignKeys: readonly SqlForeignKeyIRInput[] = Array.from(foreignKeysMap.values()).map(
982
1071
  (fk) => ({
983
1072
  columns: Object.freeze([...fk.columns]) as readonly string[],
984
1073
  referencedTable: fk.referencedTable,
@@ -987,6 +1076,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
987
1076
  name: fk.name,
988
1077
  ...ifDefined('onDelete', mapReferentialAction(fk.deleteRule)),
989
1078
  ...ifDefined('onUpdate', mapReferentialAction(fk.updateRule)),
1079
+ dependsOn: [
1080
+ postgresTableDependsOn(fk.referencedSchema, fk.referencedTable),
1081
+ ...postgresColumnDependsOn(schema, tableName, fk.columns),
1082
+ ],
990
1083
  }),
991
1084
  );
992
1085
 
@@ -1008,9 +1101,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1008
1101
  });
1009
1102
  }
1010
1103
  }
1011
- const uniques: readonly SqlUniqueIR[] = Array.from(uniquesMap.values()).map((uq) => ({
1104
+ const uniques: readonly SqlUniqueIRInput[] = Array.from(uniquesMap.values()).map((uq) => ({
1012
1105
  columns: Object.freeze([...uq.columns]) as readonly string[],
1013
1106
  name: uq.name,
1107
+ dependsOn: postgresColumnDependsOn(schema, tableName, uq.columns),
1014
1108
  }));
1015
1109
 
1016
1110
  // Process indexes
@@ -1024,8 +1118,22 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1024
1118
  options: Record<string, string> | undefined;
1025
1119
  }
1026
1120
  >();
1121
+ // An index with an expression key (e.g. `lower(email)`) reports that
1122
+ // key's row with `attname = null` (Postgres attribute numbers are
1123
+ // <= 0 for expressions, which the LEFT JOIN above can't resolve to a
1124
+ // real column). Every row for that index name is skipped below
1125
+ // rather than only the expression row, so the index never enters
1126
+ // `indexesMap` with a collapsed, misleading column list — a
1127
+ // two-column expression index silently reduced to its one real
1128
+ // column can coincide with an unrelated real single-column index,
1129
+ // and the schema differ's diff-tree node id is derived from the
1130
+ // column tuple (`sql-index/index:<columns>`), so two indexes
1131
+ // colliding on that tuple abort the diff with "duplicate id among
1132
+ // siblings" instead of a normal drift report.
1133
+ const indexNamesWithExpressionKey = new Set<string>();
1027
1134
  for (const idxRow of indexesByTable.get(tableName) ?? []) {
1028
1135
  if (!idxRow.attname) {
1136
+ indexNamesWithExpressionKey.add(idxRow.indexname);
1029
1137
  continue;
1030
1138
  }
1031
1139
  const existing = indexesMap.get(idxRow.indexname);
@@ -1046,13 +1154,41 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1046
1154
  });
1047
1155
  }
1048
1156
  }
1049
- const indexes: readonly SqlIndexIR[] = Array.from(indexesMap.values()).map((idx) => ({
1050
- columns: Object.freeze([...idx.columns]) as readonly string[],
1051
- name: idx.name,
1052
- unique: idx.unique,
1053
- ...(idx.type !== undefined && { type: idx.type }),
1054
- ...(idx.options !== undefined && { options: idx.options }),
1055
- }));
1157
+ // Two real indexes can legitimately share the exact same column tuple
1158
+ // on one table (e.g. a unique index and a redundant plain index) —
1159
+ // valid in Postgres, but the schema differ's diff-tree node id for an
1160
+ // index is the column tuple alone (`SqlIndexIR#id`, deliberately —
1161
+ // see its doc comment), so two same-tuple siblings from one
1162
+ // introspection abort the diff with "duplicate id among siblings"
1163
+ // rather than a normal drift report. Keep only one per column tuple:
1164
+ // the unique one when there is a unique/non-unique pair (a unique
1165
+ // index is a strict superset of what a plain index on the same
1166
+ // columns would add), otherwise the first by name for determinism.
1167
+ const survivingIndexes = Array.from(indexesMap.values()).filter(
1168
+ (idx) => !indexNamesWithExpressionKey.has(idx.name),
1169
+ );
1170
+ const bestByColumnTuple = new Map<string, (typeof survivingIndexes)[number]>();
1171
+ for (const idx of survivingIndexes) {
1172
+ const tupleKey = idx.columns.join(',');
1173
+ const existing = bestByColumnTuple.get(tupleKey);
1174
+ if (
1175
+ !existing ||
1176
+ (idx.unique && !existing.unique) ||
1177
+ (idx.unique === existing.unique && idx.name < existing.name)
1178
+ ) {
1179
+ bestByColumnTuple.set(tupleKey, idx);
1180
+ }
1181
+ }
1182
+ const indexes: readonly SqlIndexIRInput[] = Array.from(bestByColumnTuple.values()).map(
1183
+ (idx) => ({
1184
+ columns: Object.freeze([...idx.columns]),
1185
+ name: idx.name,
1186
+ unique: idx.unique,
1187
+ ...(idx.type !== undefined && { type: idx.type }),
1188
+ ...(idx.options !== undefined && { options: idx.options }),
1189
+ dependsOn: postgresColumnDependsOn(schema, tableName, idx.columns),
1190
+ }),
1191
+ );
1056
1192
 
1057
1193
  // Process check constraints — parse each predicate into column + value set.
1058
1194
  // Only the two shapes emitted by this slice are recognised; free-form
@@ -1069,7 +1205,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1069
1205
  }
1070
1206
  }
1071
1207
 
1072
- tables[tableName] = {
1208
+ tableInputs[tableName] = {
1073
1209
  name: tableName,
1074
1210
  columns,
1075
1211
  ...ifDefined('primaryKey', primaryKey),
@@ -1080,29 +1216,114 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1080
1216
  };
1081
1217
  }
1082
1218
 
1083
- const nativeEnumResult = await driver.query<{ typname: string }>(
1084
- `SELECT t.typname
1219
+ const nativeEnumResult = await driver.query<{ typname: string; enumvalues: unknown }>(
1220
+ `SELECT t.typname, array_agg(e.enumlabel ORDER BY e.enumsortorder) AS enumvalues
1085
1221
  FROM pg_catalog.pg_type t
1086
1222
  JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
1223
+ JOIN pg_catalog.pg_enum e ON e.enumtypid = t.oid
1087
1224
  WHERE t.typtype = 'e'
1088
1225
  AND n.nspname = $1
1226
+ GROUP BY t.typname
1089
1227
  ORDER BY t.typname`,
1090
1228
  [schema],
1091
1229
  );
1092
- const nativeEnumTypeNames = nativeEnumResult.rows.map((r) => r.typname);
1230
+ const enums = nativeEnumResult.rows.map(
1231
+ (r) =>
1232
+ new PostgresNativeEnumSchemaNode({
1233
+ typeName: r.typname,
1234
+ namespaceId: schema,
1235
+ members: parsePgNameArray(r.enumvalues),
1236
+ }),
1237
+ );
1238
+ const policiesResult = await driver.query<{
1239
+ schemaname: string;
1240
+ tablename: string;
1241
+ policyname: string;
1242
+ cmd: string;
1243
+ roles: string[];
1244
+ qual: string | null;
1245
+ with_check: string | null;
1246
+ permissive: string;
1247
+ }>(
1248
+ `SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
1249
+ FROM pg_catalog.pg_policies
1250
+ WHERE schemaname = $1
1251
+ ORDER BY tablename, policyname`,
1252
+ [schema],
1253
+ );
1254
+ const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();
1255
+ for (const row of policiesResult.rows) {
1256
+ const operation = mapPgCmd(row.cmd);
1257
+ const policyRoles = [
1258
+ ...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
1259
+ ].sort();
1260
+ const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
1261
+ const prefix = parseRlsPolicyWireName(row.policyname)?.prefix ?? row.policyname;
1262
+ const policy = new PostgresPolicySchemaNode({
1263
+ name: row.policyname,
1264
+ prefix,
1265
+ tableName: row.tablename,
1266
+ namespaceId: row.schemaname,
1267
+ operation,
1268
+ roles: policyRoles,
1269
+ ...(row.qual !== null ? { using: row.qual } : {}),
1270
+ ...(row.with_check !== null ? { withCheck: row.with_check } : {}),
1271
+ permissive,
1272
+ dependsOn: [
1273
+ postgresTableDependsOn(row.schemaname, row.tablename),
1274
+ ...policyRoles.map(postgresRoleDependsOn),
1275
+ ],
1276
+ });
1277
+ const list = policiesByTable.get(row.tablename) ?? [];
1278
+ list.push(policy);
1279
+ policiesByTable.set(row.tablename, list);
1280
+ }
1093
1281
 
1094
- const annotations = {
1095
- pg: {
1096
- schema,
1097
- version: await this.getPostgresVersion(driver),
1098
- ...(nativeEnumTypeNames.length > 0 && { nativeEnumTypeNames }),
1099
- },
1100
- };
1282
+ // RLS enablement is a table attribute (`pg_class.relrowsecurity`), not a
1283
+ // function of the policy set — a table can have RLS on with zero
1284
+ // policies (deny-all) or policies present with RLS off. relkind covers
1285
+ // both plain ('r') and partitioned ('p') tables: the table listing above
1286
+ // (`information_schema.tables`, BASE TABLE) includes partitioned parents,
1287
+ // and Postgres supports RLS on them.
1288
+ //
1289
+ // Kept as a SEPARATE query from the table listing on purpose. Folding
1290
+ // relrowsecurity into the listing would mean replacing
1291
+ // `information_schema.tables` (which filters by the connection role's
1292
+ // grants) with a raw `pg_class` scan (which does not), changing WHICH
1293
+ // tables the introspection returns — a real behavior shift, not a
1294
+ // cleanup, and one the offline golden-diff can't catch (introspection is
1295
+ // live-only). The only cost of two queries is the concurrent-DDL window:
1296
+ // a table listed but missed by this scan defaults (`?? false`) to
1297
+ // RLS-off. That default is fail-safe — the worst case downstream is a
1298
+ // spurious ENABLE (idempotent), never a spurious DISABLE.
1299
+ const rlsEnabledResult = await driver.query<{ tablename: string; rls_enabled: boolean }>(
1300
+ `SELECT c.relname AS tablename, c.relrowsecurity AS rls_enabled
1301
+ FROM pg_catalog.pg_class c
1302
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
1303
+ WHERE n.nspname = $1
1304
+ AND c.relkind IN ('r', 'p')
1305
+ ORDER BY c.relname`,
1306
+ [schema],
1307
+ );
1308
+ const rlsEnabledByTable = new Map<string, boolean>(
1309
+ rlsEnabledResult.rows.map((row) => [row.tablename, row.rls_enabled]),
1310
+ );
1311
+
1312
+ const tables: Record<string, PostgresTableSchemaNode> = {};
1313
+ for (const [tableName, input] of Object.entries(tableInputs)) {
1314
+ tables[tableName] = new PostgresTableSchemaNode({
1315
+ ...input,
1316
+ policies: policiesByTable.get(tableName) ?? [],
1317
+ rlsEnabled: rlsEnabledByTable.get(tableName) ?? false,
1318
+ });
1319
+ }
1101
1320
 
1102
- return {
1321
+ const namespace = new PostgresNamespaceSchemaNode({
1322
+ schemaName: schema,
1103
1323
  tables,
1104
- annotations,
1105
- };
1324
+ nativeEnums: enums,
1325
+ });
1326
+ return { namespace, pgVersion: await this.getPostgresVersion(driver) };
1106
1327
  }
1107
1328
 
1108
1329
  /**
@@ -1117,6 +1338,106 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1117
1338
  }
1118
1339
  }
1119
1340
 
1341
+ /**
1342
+ * Normalises a `name[]` column value from `pg_policies.roles`.
1343
+ *
1344
+ * The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
1345
+ * `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
1346
+ * literal (`{role1,role2}`) is returned as a string instead of a JS array.
1347
+ * This function accepts either form and returns a plain string array.
1348
+ *
1349
+ * The string branch honors Postgres array-literal quoting: an element
1350
+ * containing a comma, quote, backslash, brace, or significant whitespace is
1351
+ * emitted double-quoted with `\"` / `\\` escapes, and unquoted elements are
1352
+ * whitespace-trimmed — so a label like `in progress` or `say "hi"` parses to
1353
+ * its true value instead of being split or kept escaped.
1354
+ */
1355
+ export function parsePgNameArray(value: unknown): string[] {
1356
+ if (Array.isArray(value)) {
1357
+ return value.map(String);
1358
+ }
1359
+ if (typeof value !== 'string') {
1360
+ return [];
1361
+ }
1362
+ const trimmed = value.trim();
1363
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
1364
+ return [];
1365
+ }
1366
+ const inner = trimmed.slice(1, -1);
1367
+ if (inner === '') {
1368
+ return [];
1369
+ }
1370
+
1371
+ const elements: string[] = [];
1372
+ let current = '';
1373
+ let inQuotes = false;
1374
+ let wasQuoted = false;
1375
+ const pushCurrent = () => {
1376
+ elements.push(wasQuoted ? current : current.trim());
1377
+ current = '';
1378
+ wasQuoted = false;
1379
+ };
1380
+ let i = 0;
1381
+ while (i < inner.length) {
1382
+ const char = inner.charAt(i);
1383
+ if (inQuotes) {
1384
+ if (char === '\\') {
1385
+ current += inner[i + 1] ?? '';
1386
+ i += 2;
1387
+ continue;
1388
+ }
1389
+ if (char === '"') {
1390
+ inQuotes = false;
1391
+ i++;
1392
+ continue;
1393
+ }
1394
+ current += char;
1395
+ i++;
1396
+ continue;
1397
+ }
1398
+ if (char === '"') {
1399
+ inQuotes = true;
1400
+ wasQuoted = true;
1401
+ i++;
1402
+ continue;
1403
+ }
1404
+ if (char === ',') {
1405
+ pushCurrent();
1406
+ i++;
1407
+ continue;
1408
+ }
1409
+ current += char;
1410
+ i++;
1411
+ }
1412
+ // A still-open quote means the literal was malformed (e.g. `{"unterminated}`);
1413
+ // reject rather than emit the partial value.
1414
+ if (inQuotes) {
1415
+ return [];
1416
+ }
1417
+ pushCurrent();
1418
+ return elements;
1419
+ }
1420
+
1421
+ /**
1422
+ * Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
1423
+ * The `pg_policies` view renders the internal command code as an uppercase
1424
+ * English keyword; this function lowercases to match the IR type.
1425
+ */
1426
+ function mapPgCmd(cmd: string): RlsPolicyOperation {
1427
+ switch (cmd.toUpperCase()) {
1428
+ case 'SELECT':
1429
+ return 'select';
1430
+ case 'INSERT':
1431
+ return 'insert';
1432
+ case 'UPDATE':
1433
+ return 'update';
1434
+ case 'DELETE':
1435
+ return 'delete';
1436
+ default:
1437
+ return 'all';
1438
+ }
1439
+ }
1440
+
1120
1441
  /**
1121
1442
  * Extracts the namespace coordinate ids declared on a contract's storage,
1122
1443
  * or returns an empty array when no contract (or no storage / namespaces)
@@ -1133,6 +1454,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
1133
1454
  }
1134
1455
 
1135
1456
  function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
1457
+ if (formattedType.endsWith('[]')) {
1458
+ return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
1459
+ }
1136
1460
  if (formattedType === 'integer') {
1137
1461
  return 'int4';
1138
1462
  }
@@ -1210,6 +1534,44 @@ function mapReferentialAction(rule: string): SqlReferentialAction | undefined {
1210
1534
  return mapped;
1211
1535
  }
1212
1536
 
1537
+ /**
1538
+ * A FK/policy dependency chain, mirroring the shape the expected-side
1539
+ * derivation stamps (`contractToPostgresDatabaseSchemaNode`): the database
1540
+ * root's fixed sentinel id, then the namespace, then the table.
1541
+ */
1542
+ function postgresTableDependsOn(namespaceId: string, tableName: string): SchemaNodeRef {
1543
+ return [
1544
+ { nodeKind: PostgresSchemaNodeKind.database, id: 'database' },
1545
+ { nodeKind: PostgresSchemaNodeKind.namespace, id: namespaceId },
1546
+ { nodeKind: PostgresSchemaNodeKind.table, id: tableName },
1547
+ ];
1548
+ }
1549
+
1550
+ /** A policy's dependency chain onto one of the roles it grants to. */
1551
+ function postgresRoleDependsOn(role: string): SchemaNodeRef {
1552
+ return [
1553
+ { nodeKind: PostgresSchemaNodeKind.database, id: 'database' },
1554
+ { nodeKind: PostgresSchemaNodeKind.role, id: role },
1555
+ ];
1556
+ }
1557
+
1558
+ /**
1559
+ * The chains from a table-child object (foreign key, index, unique, primary
1560
+ * key) to each of the own columns it is built on — the introspection-side
1561
+ * mirror of `contractToPostgresDatabaseSchemaNode`'s `columnDependsOn`. An
1562
+ * object is dropped before the columns it covers.
1563
+ */
1564
+ function postgresColumnDependsOn(
1565
+ namespaceId: string,
1566
+ tableName: string,
1567
+ columns: readonly string[],
1568
+ ): SchemaNodeRef[] {
1569
+ return columns.map((column) => [
1570
+ ...postgresTableDependsOn(namespaceId, tableName),
1571
+ { nodeKind: RelationalSchemaNodeKind.column, id: `column:${column}` },
1572
+ ]);
1573
+ }
1574
+
1213
1575
  /**
1214
1576
  * Groups an array of objects by a specified key.
1215
1577
  * Returns a Map for O(1) lookup by group key.
@@ -1376,6 +1738,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
1376
1738
  );
1377
1739
  }
1378
1740
 
1741
+ function pgRenderArrayElement(el: unknown): string {
1742
+ if (el === null) return 'NULL';
1743
+ if (typeof el === 'number' || typeof el === 'boolean') return String(el);
1744
+ if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
1745
+ return `'${escapeLiteral(JSON.stringify(el))}'`;
1746
+ }
1747
+
1748
+ function pgRenderArrayLiteral(elements: unknown[]): string {
1749
+ if (elements.length === 0) return "'{}'";
1750
+ return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
1751
+ }
1752
+
1379
1753
  function pgInlineLiteral(wire: unknown, nativeType: string): string {
1380
1754
  if (wire === null) return 'NULL';
1381
1755
  if (typeof wire === 'boolean') return wire ? 'true' : 'false';
@@ -1407,6 +1781,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
1407
1781
  .join('');
1408
1782
  return `'\\x${hex}'::${nativeType}`;
1409
1783
  }
1784
+ if (Array.isArray(wire) && nativeType.endsWith('[]')) {
1785
+ return pgRenderArrayLiteral(wire);
1786
+ }
1410
1787
  if (typeof wire === 'object') {
1411
1788
  const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
1412
1789
  return `${quoted}::${nativeType}`;
@@ -1477,6 +1854,9 @@ function pgRenderDdlConstraint(constraint: DdlTableConstraint): string {
1477
1854
  }
1478
1855
  return sql;
1479
1856
  }
1857
+ if (constraint.kind === 'check-expression') {
1858
+ return `CONSTRAINT ${quoteIdentifier(constraint.name)} CHECK (${constraint.expression})`;
1859
+ }
1480
1860
  const cols = constraint.columns.map(quoteIdentifier).join(', ');
1481
1861
  if (constraint.name !== undefined) {
1482
1862
  return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
@@ -1512,6 +1892,27 @@ function pgRenderCreateSchema(node: PostgresCreateSchema): SqlExecuteRequest {
1512
1892
  };
1513
1893
  }
1514
1894
 
1895
+ function pgRenderCreateType(node: PostgresCreateType): SqlExecuteRequest {
1896
+ const typeRef = node.schema
1897
+ ? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.name)}`
1898
+ : quoteIdentifier(node.name);
1899
+ const values = node.values.map((value) => `'${escapeLiteral(value)}'`).join(', ');
1900
+ return {
1901
+ sql: `CREATE TYPE ${typeRef} AS ENUM (${values})`,
1902
+ params: [],
1903
+ };
1904
+ }
1905
+
1906
+ function pgRenderDropType(node: PostgresDropType): SqlExecuteRequest {
1907
+ const typeRef = node.schema
1908
+ ? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.name)}`
1909
+ : quoteIdentifier(node.name);
1910
+ return {
1911
+ sql: `DROP TYPE ${typeRef}`,
1912
+ params: [],
1913
+ };
1914
+ }
1915
+
1515
1916
  async function pgRenderAlterTable(
1516
1917
  node: PostgresAlterTable,
1517
1918
  codecLookup: CodecLookup,
@@ -1524,6 +1925,9 @@ async function pgRenderAlterTable(
1524
1925
  const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
1525
1926
  return `ADD COLUMN ${colFragment}`;
1526
1927
  },
1928
+ dropDefault(action: DropDefaultAction): Promise<string> {
1929
+ return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
1930
+ },
1527
1931
  };
1528
1932
  const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
1529
1933
  return {
@@ -1532,6 +1936,53 @@ async function pgRenderAlterTable(
1532
1936
  };
1533
1937
  }
1534
1938
 
1939
+ const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
1940
+ select: 'SELECT',
1941
+ insert: 'INSERT',
1942
+ update: 'UPDATE',
1943
+ delete: 'DELETE',
1944
+ all: 'ALL',
1945
+ };
1946
+
1947
+ function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
1948
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1949
+ const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
1950
+ const command = POLICY_OPERATION_SQL[node.operation];
1951
+ const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
1952
+ let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
1953
+ if (node.using !== undefined) {
1954
+ sql += ` USING (${node.using})`;
1955
+ }
1956
+ if (node.withCheck !== undefined) {
1957
+ sql += ` WITH CHECK (${node.withCheck})`;
1958
+ }
1959
+ return { sql, params: [] };
1960
+ }
1961
+
1962
+ function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
1963
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1964
+ return {
1965
+ sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
1966
+ params: [],
1967
+ };
1968
+ }
1969
+
1970
+ function pgRenderAlterPolicyRename(node: PostgresAlterPolicyRename): SqlExecuteRequest {
1971
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1972
+ return {
1973
+ sql: `ALTER POLICY ${quoteIdentifier(node.name)} ON ${tableRef} RENAME TO ${quoteIdentifier(node.newName)}`,
1974
+ params: [],
1975
+ };
1976
+ }
1977
+
1978
+ function pgRenderDisableRowLevelSecurity(node: PostgresDisableRowLevelSecurity): SqlExecuteRequest {
1979
+ const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
1980
+ return {
1981
+ sql: `ALTER TABLE ${tableRef} DISABLE ROW LEVEL SECURITY`,
1982
+ params: [],
1983
+ };
1984
+ }
1985
+
1535
1986
  async function pgRenderDdlExecuteRequest(
1536
1987
  ast: PostgresDdlNode,
1537
1988
  codecLookup: CodecLookup,
@@ -1539,7 +1990,15 @@ async function pgRenderDdlExecuteRequest(
1539
1990
  const visitor = {
1540
1991
  createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
1541
1992
  createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
1993
+ createType: (node: PostgresCreateType) => Promise.resolve(pgRenderCreateType(node)),
1994
+ dropType: (node: PostgresDropType) => Promise.resolve(pgRenderDropType(node)),
1542
1995
  alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
1996
+ createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
1997
+ dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
1998
+ alterPolicyRename: (node: PostgresAlterPolicyRename) =>
1999
+ Promise.resolve(pgRenderAlterPolicyRename(node)),
2000
+ disableRowLevelSecurity: (node: PostgresDisableRowLevelSecurity) =>
2001
+ Promise.resolve(pgRenderDisableRowLevelSecurity(node)),
1543
2002
  };
1544
2003
  return ast.accept(visitor);
1545
2004
  }