@prisma-next/adapter-postgres 0.14.0-dev.5 → 0.14.0-dev.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-CwkcdpM_.mjs → adapter-bOgoyFDq.mjs} +4 -3
- package/dist/adapter-bOgoyFDq.mjs.map +1 -0
- package/dist/adapter.d.mts +1 -1
- package/dist/adapter.d.mts.map +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/{control-adapter-Dspz5uKp.mjs → control-adapter-CkLNE6V5.mjs} +177 -55
- package/dist/control-adapter-CkLNE6V5.mjs.map +1 -0
- package/dist/control.d.mts +17 -11
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +2 -2
- package/dist/{descriptor-meta-DOgMfoqm.mjs → descriptor-meta-DnWlLrI6.mjs} +3 -2
- package/dist/descriptor-meta-DnWlLrI6.mjs.map +1 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.mjs +2 -2
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-KXRwRZU8.d.mts → types-DkGeLfUp.d.mts} +3 -6
- package/dist/types-DkGeLfUp.d.mts.map +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +23 -22
- package/src/core/adapter.ts +1 -0
- package/src/core/codec-lookup.ts +2 -1
- package/src/core/control-adapter.ts +269 -81
- package/src/core/descriptor-meta.ts +1 -0
- package/src/core/sql-renderer.ts +32 -6
- package/src/core/types.ts +2 -3
- package/src/exports/runtime.ts +2 -5
- package/dist/adapter-CwkcdpM_.mjs.map +0 -1
- package/dist/control-adapter-Dspz5uKp.mjs.map +0 -1
- package/dist/descriptor-meta-DOgMfoqm.mjs.map +0 -1
- package/dist/types-KXRwRZU8.d.mts.map +0 -1
package/package.json
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/adapter-postgres",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.51",
|
|
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.
|
|
9
|
-
"@prisma-next/contract-authoring": "0.14.0-dev.
|
|
10
|
-
"@prisma-next/errors": "0.14.0-dev.
|
|
11
|
-
"@prisma-next/family-sql": "0.14.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.14.0-dev.
|
|
13
|
-
"@prisma-next/ids": "0.14.0-dev.
|
|
14
|
-
"@prisma-next/sql-contract": "0.14.0-dev.
|
|
15
|
-
"@prisma-next/sql-contract-psl": "0.14.0-dev.
|
|
16
|
-
"@prisma-next/sql-contract-ts": "0.14.0-dev.
|
|
17
|
-
"@prisma-next/sql-operations": "0.14.0-dev.
|
|
18
|
-
"@prisma-next/sql-relational-core": "0.14.0-dev.
|
|
19
|
-
"@prisma-next/sql-runtime": "0.14.0-dev.
|
|
20
|
-
"@prisma-next/sql-schema-ir": "0.14.0-dev.
|
|
21
|
-
"@prisma-next/target-postgres": "0.14.0-dev.
|
|
22
|
-
"@prisma-next/utils": "0.14.0-dev.
|
|
8
|
+
"@prisma-next/contract": "0.14.0-dev.51",
|
|
9
|
+
"@prisma-next/contract-authoring": "0.14.0-dev.51",
|
|
10
|
+
"@prisma-next/errors": "0.14.0-dev.51",
|
|
11
|
+
"@prisma-next/family-sql": "0.14.0-dev.51",
|
|
12
|
+
"@prisma-next/framework-components": "0.14.0-dev.51",
|
|
13
|
+
"@prisma-next/ids": "0.14.0-dev.51",
|
|
14
|
+
"@prisma-next/sql-contract": "0.14.0-dev.51",
|
|
15
|
+
"@prisma-next/sql-contract-psl": "0.14.0-dev.51",
|
|
16
|
+
"@prisma-next/sql-contract-ts": "0.14.0-dev.51",
|
|
17
|
+
"@prisma-next/sql-operations": "0.14.0-dev.51",
|
|
18
|
+
"@prisma-next/sql-relational-core": "0.14.0-dev.51",
|
|
19
|
+
"@prisma-next/sql-runtime": "0.14.0-dev.51",
|
|
20
|
+
"@prisma-next/sql-schema-ir": "0.14.0-dev.51",
|
|
21
|
+
"@prisma-next/target-postgres": "0.14.0-dev.51",
|
|
22
|
+
"@prisma-next/utils": "0.14.0-dev.51",
|
|
23
23
|
"arktype": "^2.2.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@prisma-next/cli": "0.14.0-dev.
|
|
27
|
-
"@prisma-next/driver-postgres": "0.14.0-dev.
|
|
28
|
-
"@prisma-next/migration-tools": "0.14.0-dev.
|
|
29
|
-
"@prisma-next/
|
|
30
|
-
"@prisma-next/
|
|
31
|
-
"@prisma-next/
|
|
26
|
+
"@prisma-next/cli": "0.14.0-dev.51",
|
|
27
|
+
"@prisma-next/driver-postgres": "0.14.0-dev.51",
|
|
28
|
+
"@prisma-next/migration-tools": "0.14.0-dev.51",
|
|
29
|
+
"@prisma-next/psl-parser": "0.14.0-dev.51",
|
|
30
|
+
"@prisma-next/test-utils": "0.14.0-dev.51",
|
|
31
|
+
"@prisma-next/tsconfig": "0.14.0-dev.51",
|
|
32
|
+
"@prisma-next/tsdown": "0.14.0-dev.51",
|
|
32
33
|
"pathe": "^2.0.3",
|
|
33
34
|
"tsdown": "0.22.1",
|
|
34
35
|
"typescript": "5.9.3",
|
package/src/core/adapter.ts
CHANGED
package/src/core/codec-lookup.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { postgresCodecRegistry } from '@prisma-next/target-postgres/codecs';
|
|
|
10
10
|
* Extension codecs (e.g. `pg/vector@1` from `@prisma-next/extension-pgvector`) are intentionally NOT included here: a bare adapter cannot see extensions. Stack-composed paths (`SqlControlAdapterDescriptor.create(stack)` / `SqlRuntimeAdapterDescriptor.create(stack)`) supply the broader, extension-inclusive registry at construction time.
|
|
11
11
|
*/
|
|
12
12
|
export function createPostgresBuiltinCodecLookup(): CodecRegistry {
|
|
13
|
+
const descriptors = Array.from(postgresCodecRegistry.values());
|
|
13
14
|
return extractCodecLookup([
|
|
14
15
|
{
|
|
15
16
|
id: 'postgres-builtin-codecs',
|
|
16
|
-
types: { codecTypes: { codecDescriptors:
|
|
17
|
+
types: { codecTypes: { codecDescriptors: descriptors } },
|
|
17
18
|
},
|
|
18
19
|
]);
|
|
19
20
|
}
|
|
@@ -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
|
-
|
|
31
|
+
PrimaryKeyInput,
|
|
32
32
|
SqlCheckConstraintIRInput,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
SqlColumnIRInput,
|
|
34
|
+
SqlForeignKeyIRInput,
|
|
35
|
+
SqlIndexIRInput,
|
|
36
36
|
SqlReferentialAction,
|
|
37
|
-
|
|
38
|
-
SqlTableIR,
|
|
39
|
-
SqlUniqueIR,
|
|
37
|
+
SqlUniqueIRInput,
|
|
40
38
|
} from '@prisma-next/sql-schema-ir/types';
|
|
41
39
|
import {
|
|
42
40
|
buildControlTableBootstrapQueries,
|
|
@@ -45,14 +43,25 @@ import {
|
|
|
45
43
|
import type {
|
|
46
44
|
AddColumnAction,
|
|
47
45
|
AlterTableActionVisitor,
|
|
46
|
+
DropDefaultAction,
|
|
48
47
|
PostgresAlterTable,
|
|
48
|
+
PostgresCreatePolicy,
|
|
49
49
|
PostgresCreateSchema,
|
|
50
50
|
PostgresCreateTable,
|
|
51
51
|
PostgresDdlNode,
|
|
52
|
+
PostgresDropPolicy,
|
|
53
|
+
RlsPolicyOperation,
|
|
52
54
|
} from '@prisma-next/target-postgres/ddl';
|
|
53
55
|
import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
|
|
54
56
|
import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
|
|
55
57
|
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
58
|
+
import {
|
|
59
|
+
PostgresDatabaseSchemaNode,
|
|
60
|
+
PostgresNamespaceSchemaNode,
|
|
61
|
+
PostgresPolicySchemaNode,
|
|
62
|
+
PostgresRoleSchemaNode,
|
|
63
|
+
PostgresTableSchemaNode,
|
|
64
|
+
} from '@prisma-next/target-postgres/types';
|
|
56
65
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
57
66
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
58
67
|
import { encodeControlQueryParams } from './control-codecs';
|
|
@@ -548,26 +557,51 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
548
557
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
549
558
|
contract?: unknown,
|
|
550
559
|
schema = 'public',
|
|
551
|
-
): Promise<
|
|
560
|
+
): Promise<PostgresDatabaseSchemaNode> {
|
|
552
561
|
const declaredNamespaces = extractContractNamespaceIds(contract);
|
|
553
|
-
const
|
|
562
|
+
const resolvedSchemas =
|
|
554
563
|
declaredNamespaces.length > 0
|
|
555
|
-
? await this.
|
|
556
|
-
:
|
|
557
|
-
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
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);
|
|
561
579
|
const existingSchemas = await this.listExistingSchemas(driver);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
580
|
+
return new PostgresDatabaseSchemaNode({
|
|
581
|
+
namespaces,
|
|
582
|
+
roles,
|
|
583
|
+
existingSchemas,
|
|
584
|
+
pgVersion,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
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
|
+
);
|
|
571
605
|
}
|
|
572
606
|
|
|
573
607
|
/**
|
|
@@ -593,16 +627,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
593
627
|
}
|
|
594
628
|
|
|
595
629
|
/**
|
|
596
|
-
*
|
|
597
|
-
* the connection's `current_schema()
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
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.
|
|
601
635
|
*/
|
|
602
|
-
private async
|
|
636
|
+
private async resolveNamespaceSchemas(
|
|
603
637
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
604
638
|
namespaceIds: readonly string[],
|
|
605
|
-
): Promise<
|
|
639
|
+
): Promise<readonly string[]> {
|
|
606
640
|
const resolvedSchemas: string[] = [];
|
|
607
641
|
for (const id of namespaceIds) {
|
|
608
642
|
if (id === UNBOUND_NAMESPACE_ID) {
|
|
@@ -614,46 +648,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
614
648
|
resolvedSchemas.push(id);
|
|
615
649
|
}
|
|
616
650
|
}
|
|
617
|
-
|
|
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
|
-
};
|
|
651
|
+
return Array.from(new Set(resolvedSchemas));
|
|
646
652
|
}
|
|
647
653
|
|
|
648
654
|
/**
|
|
649
|
-
* Introspects a single Postgres schema and returns
|
|
650
|
-
*
|
|
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
|
|
651
658
|
* the per-namespace walk.
|
|
652
659
|
*/
|
|
653
660
|
private async introspectSchema(
|
|
654
661
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
655
662
|
schema: string,
|
|
656
|
-
): Promise<
|
|
663
|
+
): Promise<{ readonly namespace: PostgresNamespaceSchemaNode; readonly pgVersion: string }> {
|
|
657
664
|
// Issue the schema-wide queries one at a time. A single control connection
|
|
658
665
|
// serialises queries anyway, so Promise.all buys no parallelism here and
|
|
659
666
|
// makes pg emit a "client is already executing a query" deprecation. One
|
|
@@ -895,13 +902,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
895
902
|
constraints.add(row.constraint_name);
|
|
896
903
|
}
|
|
897
904
|
|
|
898
|
-
const
|
|
905
|
+
const tableInputs: Record<
|
|
906
|
+
string,
|
|
907
|
+
{
|
|
908
|
+
name: string;
|
|
909
|
+
columns: Record<string, SqlColumnIRInput>;
|
|
910
|
+
primaryKey?: PrimaryKeyInput;
|
|
911
|
+
foreignKeys: readonly SqlForeignKeyIRInput[];
|
|
912
|
+
uniques: readonly SqlUniqueIRInput[];
|
|
913
|
+
indexes: readonly SqlIndexIRInput[];
|
|
914
|
+
checks?: SqlCheckConstraintIRInput[];
|
|
915
|
+
}
|
|
916
|
+
> = {};
|
|
899
917
|
|
|
900
918
|
for (const tableRow of tablesResult.rows) {
|
|
901
919
|
const tableName = tableRow.table_name;
|
|
902
920
|
|
|
903
921
|
// Process columns for this table
|
|
904
|
-
const columns: Record<string,
|
|
922
|
+
const columns: Record<string, SqlColumnIRInput> = {};
|
|
905
923
|
for (const colRow of columnsByTable.get(tableName) ?? []) {
|
|
906
924
|
let nativeType = colRow.udt_name;
|
|
907
925
|
const formattedType = colRow.formatted_type
|
|
@@ -927,11 +945,36 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
927
945
|
nativeType = colRow.udt_name || colRow.data_type;
|
|
928
946
|
}
|
|
929
947
|
|
|
948
|
+
// Postgres reports array columns as data_type='ARRAY'; the element type
|
|
949
|
+
// is the `nativeType` string minus the trailing `[]`. Strip the suffix,
|
|
950
|
+
// normalize the element type to the canonical form (e.g. `integer` →
|
|
951
|
+
// `int4`), and record `many: true` so introspection consumers (verifier,
|
|
952
|
+
// psl-contract-infer) can reconstruct the full array type as needed.
|
|
953
|
+
const many = nativeType.endsWith('[]') ? true : undefined;
|
|
954
|
+
if (many) {
|
|
955
|
+
nativeType = normalizeSchemaNativeType(nativeType.slice(0, -2));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Resolved values comparable against the contract-derived expected
|
|
959
|
+
// side: the normalized full native type (`[]` appended for arrays)
|
|
960
|
+
// and the structured parse of the raw default. Raw fields stay
|
|
961
|
+
// untouched alongside — the relational walk still reads and
|
|
962
|
+
// normalizes them itself.
|
|
963
|
+
const resolvedNativeType = `${normalizeSchemaNativeType(nativeType)}${many ? '[]' : ''}`;
|
|
964
|
+
const rawDefault = colRow.column_default ?? undefined;
|
|
930
965
|
columns[colRow.column_name] = {
|
|
931
966
|
name: colRow.column_name,
|
|
932
967
|
nativeType,
|
|
933
968
|
nullable: colRow.is_nullable === 'YES',
|
|
934
|
-
...ifDefined('default',
|
|
969
|
+
...ifDefined('default', rawDefault),
|
|
970
|
+
...ifDefined('many', many),
|
|
971
|
+
resolvedNativeType,
|
|
972
|
+
...ifDefined(
|
|
973
|
+
'resolvedDefault',
|
|
974
|
+
rawDefault !== undefined
|
|
975
|
+
? parsePostgresDefault(rawDefault, resolvedNativeType)
|
|
976
|
+
: undefined,
|
|
977
|
+
),
|
|
935
978
|
};
|
|
936
979
|
}
|
|
937
980
|
|
|
@@ -940,7 +983,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
940
983
|
const primaryKeyColumns = pkRows
|
|
941
984
|
.sort((a, b) => a.ordinal_position - b.ordinal_position)
|
|
942
985
|
.map((row) => row.column_name);
|
|
943
|
-
const primaryKey:
|
|
986
|
+
const primaryKey: PrimaryKeyInput | undefined =
|
|
944
987
|
primaryKeyColumns.length > 0
|
|
945
988
|
? {
|
|
946
989
|
columns: primaryKeyColumns,
|
|
@@ -978,7 +1021,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
978
1021
|
});
|
|
979
1022
|
}
|
|
980
1023
|
}
|
|
981
|
-
const foreignKeys: readonly
|
|
1024
|
+
const foreignKeys: readonly SqlForeignKeyIRInput[] = Array.from(foreignKeysMap.values()).map(
|
|
982
1025
|
(fk) => ({
|
|
983
1026
|
columns: Object.freeze([...fk.columns]) as readonly string[],
|
|
984
1027
|
referencedTable: fk.referencedTable,
|
|
@@ -1008,7 +1051,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1008
1051
|
});
|
|
1009
1052
|
}
|
|
1010
1053
|
}
|
|
1011
|
-
const uniques: readonly
|
|
1054
|
+
const uniques: readonly SqlUniqueIRInput[] = Array.from(uniquesMap.values()).map((uq) => ({
|
|
1012
1055
|
columns: Object.freeze([...uq.columns]) as readonly string[],
|
|
1013
1056
|
name: uq.name,
|
|
1014
1057
|
}));
|
|
@@ -1046,7 +1089,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1046
1089
|
});
|
|
1047
1090
|
}
|
|
1048
1091
|
}
|
|
1049
|
-
const indexes: readonly
|
|
1092
|
+
const indexes: readonly SqlIndexIRInput[] = Array.from(indexesMap.values()).map((idx) => ({
|
|
1050
1093
|
columns: Object.freeze([...idx.columns]) as readonly string[],
|
|
1051
1094
|
name: idx.name,
|
|
1052
1095
|
unique: idx.unique,
|
|
@@ -1069,7 +1112,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1069
1112
|
}
|
|
1070
1113
|
}
|
|
1071
1114
|
|
|
1072
|
-
|
|
1115
|
+
tableInputs[tableName] = {
|
|
1073
1116
|
name: tableName,
|
|
1074
1117
|
columns,
|
|
1075
1118
|
...ifDefined('primaryKey', primaryKey),
|
|
@@ -1090,19 +1133,61 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1090
1133
|
[schema],
|
|
1091
1134
|
);
|
|
1092
1135
|
const nativeEnumTypeNames = nativeEnumResult.rows.map((r) => r.typname);
|
|
1136
|
+
const policiesResult = await driver.query<{
|
|
1137
|
+
schemaname: string;
|
|
1138
|
+
tablename: string;
|
|
1139
|
+
policyname: string;
|
|
1140
|
+
cmd: string;
|
|
1141
|
+
roles: string[];
|
|
1142
|
+
qual: string | null;
|
|
1143
|
+
with_check: string | null;
|
|
1144
|
+
permissive: string;
|
|
1145
|
+
}>(
|
|
1146
|
+
`SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
|
|
1147
|
+
FROM pg_catalog.pg_policies
|
|
1148
|
+
WHERE schemaname = $1
|
|
1149
|
+
ORDER BY tablename, policyname`,
|
|
1150
|
+
[schema],
|
|
1151
|
+
);
|
|
1152
|
+
const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();
|
|
1153
|
+
for (const row of policiesResult.rows) {
|
|
1154
|
+
const operation = mapPgCmd(row.cmd);
|
|
1155
|
+
const policyRoles = [
|
|
1156
|
+
...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
|
|
1157
|
+
].sort();
|
|
1158
|
+
const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
|
|
1159
|
+
const hashSuffixMatch = /^(.+)_([0-9a-f]{8})$/.exec(row.policyname);
|
|
1160
|
+
const prefix = hashSuffixMatch?.[1] ?? row.policyname;
|
|
1161
|
+
const policy = new PostgresPolicySchemaNode({
|
|
1162
|
+
name: row.policyname,
|
|
1163
|
+
prefix,
|
|
1164
|
+
tableName: row.tablename,
|
|
1165
|
+
namespaceId: row.schemaname,
|
|
1166
|
+
operation,
|
|
1167
|
+
roles: policyRoles,
|
|
1168
|
+
...(row.qual !== null ? { using: row.qual } : {}),
|
|
1169
|
+
...(row.with_check !== null ? { withCheck: row.with_check } : {}),
|
|
1170
|
+
permissive,
|
|
1171
|
+
});
|
|
1172
|
+
const list = policiesByTable.get(row.tablename) ?? [];
|
|
1173
|
+
list.push(policy);
|
|
1174
|
+
policiesByTable.set(row.tablename, list);
|
|
1175
|
+
}
|
|
1093
1176
|
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1177
|
+
const tables: Record<string, PostgresTableSchemaNode> = {};
|
|
1178
|
+
for (const [tableName, input] of Object.entries(tableInputs)) {
|
|
1179
|
+
tables[tableName] = new PostgresTableSchemaNode({
|
|
1180
|
+
...input,
|
|
1181
|
+
policies: policiesByTable.get(tableName) ?? [],
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1101
1184
|
|
|
1102
|
-
|
|
1185
|
+
const namespace = new PostgresNamespaceSchemaNode({
|
|
1186
|
+
schemaName: schema,
|
|
1103
1187
|
tables,
|
|
1104
|
-
|
|
1105
|
-
};
|
|
1188
|
+
nativeEnumTypeNames,
|
|
1189
|
+
});
|
|
1190
|
+
return { namespace, pgVersion: await this.getPostgresVersion(driver) };
|
|
1106
1191
|
}
|
|
1107
1192
|
|
|
1108
1193
|
/**
|
|
@@ -1117,6 +1202,52 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1117
1202
|
}
|
|
1118
1203
|
}
|
|
1119
1204
|
|
|
1205
|
+
/**
|
|
1206
|
+
* Normalises a `name[]` column value from `pg_policies.roles`.
|
|
1207
|
+
*
|
|
1208
|
+
* The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
|
|
1209
|
+
* `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
|
|
1210
|
+
* literal (`{role1,role2}`) is returned as a string instead of a JS array.
|
|
1211
|
+
* This function accepts either form and returns a plain string array.
|
|
1212
|
+
*/
|
|
1213
|
+
function parsePgNameArray(value: unknown): string[] {
|
|
1214
|
+
if (Array.isArray(value)) {
|
|
1215
|
+
return value as string[];
|
|
1216
|
+
}
|
|
1217
|
+
if (typeof value !== 'string') {
|
|
1218
|
+
return [];
|
|
1219
|
+
}
|
|
1220
|
+
const trimmed = value.trim();
|
|
1221
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
1222
|
+
return [];
|
|
1223
|
+
}
|
|
1224
|
+
const inner = trimmed.slice(1, -1);
|
|
1225
|
+
if (inner === '') {
|
|
1226
|
+
return [];
|
|
1227
|
+
}
|
|
1228
|
+
return inner.split(',').map((s) => s.trim());
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
|
|
1233
|
+
* The `pg_policies` view renders the internal command code as an uppercase
|
|
1234
|
+
* English keyword; this function lowercases to match the IR type.
|
|
1235
|
+
*/
|
|
1236
|
+
function mapPgCmd(cmd: string): RlsPolicyOperation {
|
|
1237
|
+
switch (cmd.toUpperCase()) {
|
|
1238
|
+
case 'SELECT':
|
|
1239
|
+
return 'select';
|
|
1240
|
+
case 'INSERT':
|
|
1241
|
+
return 'insert';
|
|
1242
|
+
case 'UPDATE':
|
|
1243
|
+
return 'update';
|
|
1244
|
+
case 'DELETE':
|
|
1245
|
+
return 'delete';
|
|
1246
|
+
default:
|
|
1247
|
+
return 'all';
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1120
1251
|
/**
|
|
1121
1252
|
* Extracts the namespace coordinate ids declared on a contract's storage,
|
|
1122
1253
|
* or returns an empty array when no contract (or no storage / namespaces)
|
|
@@ -1133,6 +1264,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
|
|
|
1133
1264
|
}
|
|
1134
1265
|
|
|
1135
1266
|
function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
|
|
1267
|
+
if (formattedType.endsWith('[]')) {
|
|
1268
|
+
return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
|
|
1269
|
+
}
|
|
1136
1270
|
if (formattedType === 'integer') {
|
|
1137
1271
|
return 'int4';
|
|
1138
1272
|
}
|
|
@@ -1376,6 +1510,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
|
|
|
1376
1510
|
);
|
|
1377
1511
|
}
|
|
1378
1512
|
|
|
1513
|
+
function pgRenderArrayElement(el: unknown): string {
|
|
1514
|
+
if (el === null) return 'NULL';
|
|
1515
|
+
if (typeof el === 'number' || typeof el === 'boolean') return String(el);
|
|
1516
|
+
if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
|
|
1517
|
+
return `'${escapeLiteral(JSON.stringify(el))}'`;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function pgRenderArrayLiteral(elements: unknown[]): string {
|
|
1521
|
+
if (elements.length === 0) return "'{}'";
|
|
1522
|
+
return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1379
1525
|
function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
1380
1526
|
if (wire === null) return 'NULL';
|
|
1381
1527
|
if (typeof wire === 'boolean') return wire ? 'true' : 'false';
|
|
@@ -1407,6 +1553,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
|
1407
1553
|
.join('');
|
|
1408
1554
|
return `'\\x${hex}'::${nativeType}`;
|
|
1409
1555
|
}
|
|
1556
|
+
if (Array.isArray(wire) && nativeType.endsWith('[]')) {
|
|
1557
|
+
return pgRenderArrayLiteral(wire);
|
|
1558
|
+
}
|
|
1410
1559
|
if (typeof wire === 'object') {
|
|
1411
1560
|
const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
|
|
1412
1561
|
return `${quoted}::${nativeType}`;
|
|
@@ -1477,6 +1626,9 @@ function pgRenderDdlConstraint(constraint: DdlTableConstraint): string {
|
|
|
1477
1626
|
}
|
|
1478
1627
|
return sql;
|
|
1479
1628
|
}
|
|
1629
|
+
if (constraint.kind === 'check-expression') {
|
|
1630
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} CHECK (${constraint.expression})`;
|
|
1631
|
+
}
|
|
1480
1632
|
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
1481
1633
|
if (constraint.name !== undefined) {
|
|
1482
1634
|
return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
|
|
@@ -1524,6 +1676,9 @@ async function pgRenderAlterTable(
|
|
|
1524
1676
|
const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
|
|
1525
1677
|
return `ADD COLUMN ${colFragment}`;
|
|
1526
1678
|
},
|
|
1679
|
+
dropDefault(action: DropDefaultAction): Promise<string> {
|
|
1680
|
+
return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
|
|
1681
|
+
},
|
|
1527
1682
|
};
|
|
1528
1683
|
const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
|
|
1529
1684
|
return {
|
|
@@ -1532,6 +1687,37 @@ async function pgRenderAlterTable(
|
|
|
1532
1687
|
};
|
|
1533
1688
|
}
|
|
1534
1689
|
|
|
1690
|
+
const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
|
|
1691
|
+
select: 'SELECT',
|
|
1692
|
+
insert: 'INSERT',
|
|
1693
|
+
update: 'UPDATE',
|
|
1694
|
+
delete: 'DELETE',
|
|
1695
|
+
all: 'ALL',
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
|
|
1699
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1700
|
+
const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
|
|
1701
|
+
const command = POLICY_OPERATION_SQL[node.operation];
|
|
1702
|
+
const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
|
|
1703
|
+
let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
|
|
1704
|
+
if (node.using !== undefined) {
|
|
1705
|
+
sql += ` USING (${node.using})`;
|
|
1706
|
+
}
|
|
1707
|
+
if (node.withCheck !== undefined) {
|
|
1708
|
+
sql += ` WITH CHECK (${node.withCheck})`;
|
|
1709
|
+
}
|
|
1710
|
+
return { sql, params: [] };
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
|
|
1714
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1715
|
+
return {
|
|
1716
|
+
sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
|
|
1717
|
+
params: [],
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1535
1721
|
async function pgRenderDdlExecuteRequest(
|
|
1536
1722
|
ast: PostgresDdlNode,
|
|
1537
1723
|
codecLookup: CodecLookup,
|
|
@@ -1540,6 +1726,8 @@ async function pgRenderDdlExecuteRequest(
|
|
|
1540
1726
|
createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
|
|
1541
1727
|
createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
|
|
1542
1728
|
alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
|
|
1729
|
+
createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
|
|
1730
|
+
dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
|
|
1543
1731
|
};
|
|
1544
1732
|
return ast.accept(visitor);
|
|
1545
1733
|
}
|