@prisma-next/adapter-postgres 0.14.0-dev.39 → 0.14.0-dev.40

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,16 +1,14 @@
1
- import { SchemaDiffIssue } from "@prisma-next/framework-components/control";
2
1
  import { AnyQueryAst, DdlNode, LoweredStatement, LowererContext, MarkerReadResult, SqlExecuteRequest } from "@prisma-next/sql-relational-core/ast";
3
2
  import { parsePostgresDefault, parsePostgresDefault as parsePostgresDefault$1 } from "@prisma-next/target-postgres/default-normalizer";
4
3
  import { normalizeSchemaNativeType, normalizeSchemaNativeType as normalizeSchemaNativeType$1 } from "@prisma-next/target-postgres/native-type-normalizer";
5
4
  import { SqlEscapeError, escapeLiteral, qualifyName, quoteIdentifier } from "@prisma-next/target-postgres/sql-utils";
6
- import { PostgresSchemaIR } from "@prisma-next/target-postgres/types";
5
+ import { PostgresDatabaseSchemaNode } from "@prisma-next/target-postgres/types";
7
6
  import { SqlControlAdapterDescriptor } from "@prisma-next/family-sql/control";
8
7
  import { PostgresDdlNode } from "@prisma-next/target-postgres/ddl";
9
8
  import { CodecRegistry } from "@prisma-next/framework-components/codec";
10
- import { SqlControlDriverInstance, SqlStorage } from "@prisma-next/sql-contract/types";
11
- import { Contract, ContractMarkerRecord, LedgerEntryRecord } from "@prisma-next/contract/types";
9
+ import { SqlControlDriverInstance } from "@prisma-next/sql-contract/types";
10
+ import { ContractMarkerRecord, LedgerEntryRecord } from "@prisma-next/contract/types";
12
11
  import { SqlControlAdapter } from "@prisma-next/family-sql/control-adapter";
13
- import { SqlSchemaIR } from "@prisma-next/sql-schema-ir/types";
14
12
 
15
13
  //#region src/core/codec-lookup.d.ts
16
14
  /**
@@ -43,7 +41,6 @@ declare class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
43
41
  * before comparison with contract native types.
44
42
  */
45
43
  readonly normalizeNativeType: typeof normalizeSchemaNativeType$1;
46
- collectSchemaDiffIssues(contract: Contract<SqlStorage>, schema: SqlSchemaIR): readonly SchemaDiffIssue[];
47
44
  bootstrapControlTableQueries(): readonly DdlNode[];
48
45
  bootstrapSignMarkerQueries(): readonly DdlNode[];
49
46
  /**
@@ -151,7 +148,12 @@ declare class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
151
148
  * @param schema - Schema name to introspect when no contract is provided (defaults to 'public')
152
149
  * @returns Promise resolving to SqlSchemaIR representing the live database schema
153
150
  */
154
- introspect(driver: SqlControlDriverInstance<'postgres'>, contract?: unknown, schema?: string): Promise<PostgresSchemaIR>;
151
+ introspect(driver: SqlControlDriverInstance<'postgres'>, contract?: unknown, schema?: string): Promise<PostgresDatabaseSchemaNode>;
152
+ /**
153
+ * Reads cluster-scoped database roles. Roles are not schema-qualified, so
154
+ * this is queried once for the whole database rather than per namespace.
155
+ */
156
+ private introspectRoles;
155
157
  /**
156
158
  * Lists every non-system schema present in the connected database.
157
159
  * The introspection consumer (`verifyPostgresNamespacePresence`)
@@ -162,16 +164,17 @@ declare class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
162
164
  */
163
165
  private listExistingSchemas;
164
166
  /**
165
- * Walks every declared namespace, resolving `UNBOUND_NAMESPACE_ID` to
166
- * the connection's `current_schema()`, and merges the per-schema results
167
- * into a single `SqlSchemaIR`. The merged `tables` map is flat (keyed by
168
- * table name) so callers that look up by `tableName` see every contract
169
- * table regardless of which namespace it lives in.
167
+ * Resolves the declared namespace ids to their live DDL schema names,
168
+ * mapping `UNBOUND_NAMESPACE_ID` to the connection's `current_schema()`
169
+ * and de-duplicating. The caller introspects one namespace node per
170
+ * resolved schema there is no flat cross-schema merge, so two schemas
171
+ * holding a same-named table no longer collide.
170
172
  */
171
- private introspectNamespaces;
173
+ private resolveNamespaceSchemas;
172
174
  /**
173
- * Introspects a single Postgres schema and returns a raw SqlSchemaIR
174
- * containing only the tables in that schema. Used by `introspect` as
175
+ * Introspects a single Postgres schema and returns the namespace node for
176
+ * that schema (its tables, their policies, and its native enum type names),
177
+ * alongside the cluster-scoped Postgres version. Used by `introspect` as
175
178
  * the per-namespace walk.
176
179
  */
177
180
  private introspectSchema;
@@ -1 +1 @@
1
- {"version":3,"file":"control.d.mts","names":[],"sources":["../src/core/codec-lookup.ts","../src/core/control-adapter.ts","../src/exports/control.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;iBAWgB,gCAAA,IAAoC,aAAa;;;;;;;cCgGpD,sBAAA,YAAkC,iBAAA;EAAA,SACpC,QAAA;EAAA,SACA,QAAA;EAAA,iBAEQ,aAAA;cAEL,aAAA,EAAe,aAAA;;;;AAN7B;WAcW,gBAAA,SAAgB,sBAAA;;;;;;WAOhB,mBAAA,SAAmB,2BAAA;EAE5B,uBAAA,CACE,QAAA,EAAU,QAAA,CAAS,UAAA,GACnB,MAAA,EAAQ,WAAA,YACE,eAAA;EAsBZ,4BAAA,aAAyC,OAAA;EAIzC,0BAAA,aAAuC,OAAA;EAAA;;;;;;;;EAYvC,KAAA,CAAM,GAAA,EAAK,WAAA,GAAc,eAAA,EAAiB,OAAA,EAAS,cAAA,YAA0B,gBAAA;EAyB1E;;;;;;;EAHG,qBAAA,CACJ,GAAA,EAAK,WAAA,GAAc,eAAA,EACnB,OAAA,GAAU,cAAA,YACT,OAAA,CAAQ,iBAAA;EAmDoB;;;;;;;;EAxBzB,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,WACC,OAAA,CAAQ,oBAAA;EAKL,uBAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,WACC,OAAA,CAAQ,gBAAA;EA+MD;;;;;;EApMJ,cAAA,CACJ,MAAA,EAAQ,wBAAA,eACP,OAAA,CAAQ,WAAA,SAAoB,oBAAA;EAAA,QAKjB,oBAAA;EAjJgD;;;;;;EA0MxD,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,YACC,OAAA,UAAiB,iBAAA;EAAA,QAKN,gBAAA;EA5MF;;;;;EAgQN,YAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAoBG,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAtPoC;;;;EAyRjC,YAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,YAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EA/PI;;;;EA6RD,gBAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,KAAA;IAAA,SACW,MAAA;IAAA,SACA,IAAA;IAAA,SACA,EAAA;IAAA,SACA,aAAA;IAAA,SACA,aAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAAA,QAiBW,+BAAA;EAAA,QA0BA,gBAAA;EA7SJ;;;;;;;;;;;;;;;;;;;;;;;;EAuWJ,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,QAAA,YACA,MAAA,YACC,OAAA,CAAQ,gBAAA;EA5NE;;;;;;;;EAAA,QAqPC,mBAAA;EAzND;;;;;;;EAAA,QA8OC,oBAAA;EApMD;;;;;EAAA,QAsPC,gBAAA;EAnNJ;;;EAAA,QA0uBI,kBAAA;AAAA;;;cC1rCV,yBAAA,EAA2B,2BAA2B"}
1
+ {"version":3,"file":"control.d.mts","names":[],"sources":["../src/core/codec-lookup.ts","../src/core/control-adapter.ts","../src/exports/control.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBAWgB,gCAAA,IAAoC,aAAa;;;;;;;cCqFpD,sBAAA,YAAkC,iBAAA;EAAA,SACpC,QAAA;EAAA,SACA,QAAA;EAAA,iBAEQ,aAAA;cAEL,aAAA,EAAe,aAAA;;;;AAN7B;WAcW,gBAAA,SAAgB,sBAAA;;;;;;WAOhB,mBAAA,SAAmB,2BAAA;EAE5B,4BAAA,aAAyC,OAAA;EAIzC,0BAAA,aAAuC,OAAA;EAYY;;;;;;;;EAAnD,KAAA,CAAM,GAAA,EAAK,WAAA,GAAc,eAAA,EAAiB,OAAA,EAAS,cAAA,YAA0B,gBAAA;EAuD1E;;;;;;;EAjCG,qBAAA,CACJ,GAAA,EAAK,WAAA,GAAc,eAAA,EACnB,OAAA,GAAU,cAAA,YACT,OAAA,CAAQ,iBAAA;EAkHD;;;;;;;;EAvFJ,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,WACC,OAAA,CAAQ,oBAAA;EAKL,uBAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,WACC,OAAA,CAAQ,gBAAA;EAgQR;;;;;;EArPG,cAAA,CACJ,MAAA,EAAQ,wBAAA,eACP,OAAA,CAAQ,WAAA,SAAoB,oBAAA;EAAA,QAKjB,oBAAA;EAvHL;;;;;;EAgLH,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,YACC,OAAA,UAAiB,iBAAA;EAAA,QAKN,gBAAA;EApKL;;;;;EAwNH,YAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAoBG,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAnNkB;;;;EAsPf,YAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,YAAA,UACA,WAAA;IAAA,SACW,WAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAhOD;;;;EA8PI,gBAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,KAAA,UACA,KAAA;IAAA,SACW,MAAA;IAAA,SACA,IAAA;IAAA,SACA,EAAA;IAAA,SACA,aAAA;IAAA,SACA,aAAA;IAAA,SACA,UAAA;EAAA,IAEV,OAAA;EAAA,QAiBW,+BAAA;EAAA,QA0BA,gBAAA;EA9RiB;;;;;;;;;;;;;;;;;;;;;;;;EAwVzB,UAAA,CACJ,MAAA,EAAQ,wBAAA,cACR,QAAA,YACA,MAAA,YACC,OAAA,CAAQ,0BAAA;EAnMT;;;;EAAA,QAmOY,eAAA;EAzLZ;;;;;;;;EAAA,QAgNY,mBAAA;EA1KZ;;;;;;;EAAA,QA+LY,uBAAA;EA7LZ;;;;;;EAAA,QAqNY,gBAAA;EAtGZ;;;EAAA,QAgnBY,kBAAA;AAAA;;;cC7oCV,yBAAA,EAA2B,2BAA2B"}
package/dist/control.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { r as createPostgresBuiltinCodecLookup, t as PostgresControlAdapter } from "./control-adapter-BdWK3EV1.mjs";
1
+ import { r as createPostgresBuiltinCodecLookup, t as PostgresControlAdapter } from "./control-adapter-D5hAI05c.mjs";
2
2
  import { t as postgresAdapterDescriptorMeta } from "./descriptor-meta-DnWlLrI6.mjs";
3
3
  import { parsePostgresDefault } from "@prisma-next/target-postgres/default-normalizer";
4
4
  import { normalizeSchemaNativeType } from "@prisma-next/target-postgres/native-type-normalizer";
package/dist/runtime.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as postgresRawCodecInferer, t as createPostgresAdapter } from "./adapter-i3d2XWIq.mjs";
1
+ import { n as postgresRawCodecInferer, t as createPostgresAdapter } from "./adapter-DX2HrqGc.mjs";
2
2
  import { n as postgresQueryOperations, t as postgresAdapterDescriptorMeta } from "./descriptor-meta-DnWlLrI6.mjs";
3
3
  import { extractCodecLookup } from "@prisma-next/framework-components/control";
4
4
  import { postgresCodecRegistry } from "@prisma-next/target-postgres/codecs";
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "name": "@prisma-next/adapter-postgres",
3
- "version": "0.14.0-dev.39",
3
+ "version": "0.14.0-dev.40",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "dependencies": {
8
- "@prisma-next/contract": "0.14.0-dev.39",
9
- "@prisma-next/contract-authoring": "0.14.0-dev.39",
10
- "@prisma-next/errors": "0.14.0-dev.39",
11
- "@prisma-next/family-sql": "0.14.0-dev.39",
12
- "@prisma-next/framework-components": "0.14.0-dev.39",
13
- "@prisma-next/ids": "0.14.0-dev.39",
14
- "@prisma-next/sql-contract": "0.14.0-dev.39",
15
- "@prisma-next/sql-contract-psl": "0.14.0-dev.39",
16
- "@prisma-next/sql-contract-ts": "0.14.0-dev.39",
17
- "@prisma-next/sql-operations": "0.14.0-dev.39",
18
- "@prisma-next/sql-relational-core": "0.14.0-dev.39",
19
- "@prisma-next/sql-runtime": "0.14.0-dev.39",
20
- "@prisma-next/sql-schema-ir": "0.14.0-dev.39",
21
- "@prisma-next/target-postgres": "0.14.0-dev.39",
22
- "@prisma-next/utils": "0.14.0-dev.39",
8
+ "@prisma-next/contract": "0.14.0-dev.40",
9
+ "@prisma-next/contract-authoring": "0.14.0-dev.40",
10
+ "@prisma-next/errors": "0.14.0-dev.40",
11
+ "@prisma-next/family-sql": "0.14.0-dev.40",
12
+ "@prisma-next/framework-components": "0.14.0-dev.40",
13
+ "@prisma-next/ids": "0.14.0-dev.40",
14
+ "@prisma-next/sql-contract": "0.14.0-dev.40",
15
+ "@prisma-next/sql-contract-psl": "0.14.0-dev.40",
16
+ "@prisma-next/sql-contract-ts": "0.14.0-dev.40",
17
+ "@prisma-next/sql-operations": "0.14.0-dev.40",
18
+ "@prisma-next/sql-relational-core": "0.14.0-dev.40",
19
+ "@prisma-next/sql-runtime": "0.14.0-dev.40",
20
+ "@prisma-next/sql-schema-ir": "0.14.0-dev.40",
21
+ "@prisma-next/target-postgres": "0.14.0-dev.40",
22
+ "@prisma-next/utils": "0.14.0-dev.40",
23
23
  "arktype": "^2.2.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@prisma-next/cli": "0.14.0-dev.39",
27
- "@prisma-next/driver-postgres": "0.14.0-dev.39",
28
- "@prisma-next/migration-tools": "0.14.0-dev.39",
29
- "@prisma-next/psl-parser": "0.14.0-dev.39",
30
- "@prisma-next/test-utils": "0.14.0-dev.39",
31
- "@prisma-next/tsconfig": "0.14.0-dev.39",
32
- "@prisma-next/tsdown": "0.14.0-dev.39",
26
+ "@prisma-next/cli": "0.14.0-dev.40",
27
+ "@prisma-next/driver-postgres": "0.14.0-dev.40",
28
+ "@prisma-next/migration-tools": "0.14.0-dev.40",
29
+ "@prisma-next/psl-parser": "0.14.0-dev.40",
30
+ "@prisma-next/test-utils": "0.14.0-dev.40",
31
+ "@prisma-next/tsconfig": "0.14.0-dev.40",
32
+ "@prisma-next/tsdown": "0.14.0-dev.40",
33
33
  "pathe": "^2.0.3",
34
34
  "tsdown": "0.22.1",
35
35
  "typescript": "5.9.3",
@@ -1,8 +1,4 @@
1
- import type {
2
- Contract,
3
- ContractMarkerRecord,
4
- LedgerEntryRecord,
5
- } from '@prisma-next/contract/types';
1
+ import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
6
2
  import {
7
3
  parseMarkerRowSafely,
8
4
  rethrowMarkerReadError,
@@ -11,11 +7,11 @@ import {
11
7
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
12
8
  import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
13
9
  import type { CodecLookup, CodecRegistry } from '@prisma-next/framework-components/codec';
14
- import { APP_SPACE_ID, type SchemaDiffIssue } from '@prisma-next/framework-components/control';
10
+ import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
15
11
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
16
12
  import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
17
13
  import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
18
- import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-contract/types';
14
+ import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
19
15
  import type {
20
16
  AnyQueryAst,
21
17
  CodecRef,
@@ -38,7 +34,6 @@ import type {
38
34
  SqlForeignKeyIR,
39
35
  SqlIndexIR,
40
36
  SqlReferentialAction,
41
- SqlSchemaIR,
42
37
  SqlUniqueIR,
43
38
  } from '@prisma-next/sql-schema-ir/types';
44
39
  import {
@@ -59,19 +54,13 @@ import type {
59
54
  } from '@prisma-next/target-postgres/ddl';
60
55
  import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
61
56
  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';
67
57
  import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
68
58
  import {
69
- ensurePostgresSchemaIR,
70
- isPostgresSchemaIR,
71
- PostgresRlsPolicy,
72
- PostgresRole,
73
- PostgresSchemaIR,
74
- PostgresTableIR,
59
+ PostgresDatabaseSchemaNode,
60
+ PostgresNamespaceSchemaNode,
61
+ PostgresPolicySchemaNode,
62
+ PostgresRoleSchemaNode,
63
+ PostgresTableSchemaNode,
75
64
  } from '@prisma-next/target-postgres/types';
76
65
  import { blindCast } from '@prisma-next/utils/casts';
77
66
  import { ifDefined } from '@prisma-next/utils/defined';
@@ -128,31 +117,6 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
128
117
  */
129
118
  readonly normalizeNativeType = normalizeSchemaNativeType;
130
119
 
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
-
156
120
  bootstrapControlTableQueries(): readonly DdlNode[] {
157
121
  return buildControlTableBootstrapQueries();
158
122
  }
@@ -593,23 +557,53 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
593
557
  driver: SqlControlDriverInstance<'postgres'>,
594
558
  contract?: unknown,
595
559
  schema = 'public',
596
- ): Promise<PostgresSchemaIR> {
560
+ ): Promise<PostgresDatabaseSchemaNode> {
597
561
  const declaredNamespaces = extractContractNamespaceIds(contract);
598
- const ir =
562
+ const resolvedSchemas =
599
563
  declaredNamespaces.length > 0
600
- ? await this.introspectNamespaces(driver, declaredNamespaces)
601
- : await this.introspectSchema(driver, schema);
564
+ ? await this.resolveNamespaceSchemas(driver, declaredNamespaces)
565
+ : [schema];
566
+
567
+ // Walk schemas sequentially: every introspectSchema call shares the one
568
+ // control connection, so a parallel walk only serialises behind the wire
569
+ // protocol and trips pg's "already executing a query" deprecation.
570
+ const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};
571
+ let pgVersion = 'unknown';
572
+ for (const resolved of resolvedSchemas) {
573
+ const { namespace, pgVersion: version } = await this.introspectSchema(driver, resolved);
574
+ namespaces[resolved] = namespace;
575
+ pgVersion = version;
576
+ }
577
+
578
+ const roles = await this.introspectRoles(driver);
602
579
  const existingSchemas = await this.listExistingSchemas(driver);
603
- return new PostgresSchemaIR({
604
- tables: ir.tables,
605
- pgSchemaName: ir.pgSchemaName,
606
- pgVersion: ir.pgVersion,
607
- roles: ir.roles,
580
+ return new PostgresDatabaseSchemaNode({
581
+ namespaces,
582
+ roles,
608
583
  existingSchemas,
609
- nativeEnumTypeNames: ir.nativeEnumTypeNames,
584
+ pgVersion,
610
585
  });
611
586
  }
612
587
 
588
+ /**
589
+ * Reads cluster-scoped database roles. Roles are not schema-qualified, so
590
+ * this is queried once for the whole database rather than per namespace.
591
+ */
592
+ private async introspectRoles(
593
+ driver: SqlControlDriverInstance<'postgres'>,
594
+ ): Promise<readonly PostgresRoleSchemaNode[]> {
595
+ const rolesResult = await driver.query<{ rolname: string }>(
596
+ `SELECT rolname
597
+ FROM pg_catalog.pg_roles
598
+ WHERE rolname NOT LIKE 'pg_%'
599
+ AND rolname != 'postgres'
600
+ ORDER BY rolname`,
601
+ );
602
+ return rolesResult.rows.map(
603
+ (row) => new PostgresRoleSchemaNode({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
604
+ );
605
+ }
606
+
613
607
  /**
614
608
  * Lists every non-system schema present in the connected database.
615
609
  * The introspection consumer (`verifyPostgresNamespacePresence`)
@@ -633,16 +627,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
633
627
  }
634
628
 
635
629
  /**
636
- * Walks every declared namespace, resolving `UNBOUND_NAMESPACE_ID` to
637
- * the connection's `current_schema()`, and merges the per-schema results
638
- * into a single `SqlSchemaIR`. The merged `tables` map is flat (keyed by
639
- * table name) so callers that look up by `tableName` see every contract
640
- * table regardless of which namespace it lives in.
630
+ * Resolves the declared namespace ids to their live DDL schema names,
631
+ * mapping `UNBOUND_NAMESPACE_ID` to the connection's `current_schema()`
632
+ * and de-duplicating. The caller introspects one namespace node per
633
+ * resolved schema there is no flat cross-schema merge, so two schemas
634
+ * holding a same-named table no longer collide.
641
635
  */
642
- private async introspectNamespaces(
636
+ private async resolveNamespaceSchemas(
643
637
  driver: SqlControlDriverInstance<'postgres'>,
644
638
  namespaceIds: readonly string[],
645
- ): Promise<PostgresSchemaIR> {
639
+ ): Promise<readonly string[]> {
646
640
  const resolvedSchemas: string[] = [];
647
641
  for (const id of namespaceIds) {
648
642
  if (id === UNBOUND_NAMESPACE_ID) {
@@ -654,45 +648,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
654
648
  resolvedSchemas.push(id);
655
649
  }
656
650
  }
657
- const uniqueSchemas = Array.from(new Set(resolvedSchemas));
658
-
659
- // Walk schemas sequentially: every introspectSchema call shares the one
660
- // control connection, so a parallel walk only serialises behind the wire
661
- // protocol and trips pg's "already executing a query" deprecation.
662
- const perSchema: PostgresSchemaIR[] = [];
663
- for (const schema of uniqueSchemas) {
664
- perSchema.push(await this.introspectSchema(driver, schema));
665
- }
666
-
667
- const mergedTables: Record<string, PostgresTableIR> = {};
668
- const mergedRoles: PostgresRole[] = [];
669
- for (const ir of perSchema) {
670
- for (const [tableName, table] of Object.entries(ir.tables)) {
671
- mergedTables[tableName] = table;
672
- }
673
- mergedRoles.push(...ir.roles);
674
- }
675
-
676
- const first = perSchema[0];
677
- return new PostgresSchemaIR({
678
- tables: mergedTables,
679
- pgSchemaName: first?.pgSchemaName ?? 'public',
680
- pgVersion: first?.pgVersion ?? 'unknown',
681
- roles: mergedRoles,
682
- existingSchemas: first?.existingSchemas ?? ['public'],
683
- nativeEnumTypeNames: first?.nativeEnumTypeNames ?? [],
684
- });
651
+ return Array.from(new Set(resolvedSchemas));
685
652
  }
686
653
 
687
654
  /**
688
- * Introspects a single Postgres schema and returns a raw SqlSchemaIR
689
- * containing only the tables in that schema. Used by `introspect` as
655
+ * Introspects a single Postgres schema and returns the namespace node for
656
+ * that schema (its tables, their policies, and its native enum type names),
657
+ * alongside the cluster-scoped Postgres version. Used by `introspect` as
690
658
  * the per-namespace walk.
691
659
  */
692
660
  private async introspectSchema(
693
661
  driver: SqlControlDriverInstance<'postgres'>,
694
662
  schema: string,
695
- ): Promise<PostgresSchemaIR> {
663
+ ): Promise<{ readonly namespace: PostgresNamespaceSchemaNode; readonly pgVersion: string }> {
696
664
  // Issue the schema-wide queries one at a time. A single control connection
697
665
  // serialises queries anyway, so Promise.all buys no parallelism here and
698
666
  // makes pg emit a "client is already executing a query" deprecation. One
@@ -1167,14 +1135,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1167
1135
  ORDER BY tablename, policyname`,
1168
1136
  [schema],
1169
1137
  );
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[]>();
1138
+ const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();
1178
1139
  for (const row of policiesResult.rows) {
1179
1140
  const operation = mapPgCmd(row.cmd);
1180
1141
  const policyRoles = [
@@ -1183,7 +1144,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1183
1144
  const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
1184
1145
  const hashSuffixMatch = /^(.+)_([0-9a-f]{8})$/.exec(row.policyname);
1185
1146
  const prefix = hashSuffixMatch?.[1] ?? row.policyname;
1186
- const policy = new PostgresRlsPolicy({
1147
+ const policy = new PostgresPolicySchemaNode({
1187
1148
  name: row.policyname,
1188
1149
  prefix,
1189
1150
  tableName: row.tablename,
@@ -1199,26 +1160,20 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1199
1160
  policiesByTable.set(row.tablename, list);
1200
1161
  }
1201
1162
 
1202
- const tables: Record<string, PostgresTableIR> = {};
1163
+ const tables: Record<string, PostgresTableSchemaNode> = {};
1203
1164
  for (const [tableName, input] of Object.entries(tableInputs)) {
1204
- tables[tableName] = new PostgresTableIR({
1165
+ tables[tableName] = new PostgresTableSchemaNode({
1205
1166
  ...input,
1206
- rlsPolicies: policiesByTable.get(tableName) ?? [],
1167
+ policies: policiesByTable.get(tableName) ?? [],
1207
1168
  });
1208
1169
  }
1209
1170
 
1210
- const roles: PostgresRole[] = rolesResult.rows.map(
1211
- (row) => new PostgresRole({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
1212
- );
1213
-
1214
- return new PostgresSchemaIR({
1171
+ const namespace = new PostgresNamespaceSchemaNode({
1172
+ schemaName: schema,
1215
1173
  tables,
1216
- pgSchemaName: schema,
1217
- pgVersion: await this.getPostgresVersion(driver),
1218
- roles,
1219
- existingSchemas: [],
1220
1174
  nativeEnumTypeNames,
1221
1175
  });
1176
+ return { namespace, pgVersion: await this.getPostgresVersion(driver) };
1222
1177
  }
1223
1178
 
1224
1179
  /**