@prisma-next/adapter-postgres 0.14.0-dev.4 → 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.
- package/dist/{adapter-CwkcdpM_.mjs → adapter-DX2HrqGc.mjs} +4 -3
- package/dist/adapter-DX2HrqGc.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-D5hAI05c.mjs} +172 -54
- package/dist/control-adapter-D5hAI05c.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/{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/control-adapter.ts +244 -70
- package/src/core/descriptor-meta.ts +1 -0
- package/src/core/sql-renderer.ts +11 -4
- package/src/core/types.ts +2 -3
- 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.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.
|
|
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.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.
|
|
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.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",
|
|
32
33
|
"pathe": "^2.0.3",
|
|
33
34
|
"tsdown": "0.22.1",
|
|
34
35
|
"typescript": "5.9.3",
|
package/src/core/adapter.ts
CHANGED
|
@@ -34,8 +34,6 @@ import type {
|
|
|
34
34
|
SqlForeignKeyIR,
|
|
35
35
|
SqlIndexIR,
|
|
36
36
|
SqlReferentialAction,
|
|
37
|
-
SqlSchemaIR,
|
|
38
|
-
SqlTableIR,
|
|
39
37
|
SqlUniqueIR,
|
|
40
38
|
} from '@prisma-next/sql-schema-ir/types';
|
|
41
39
|
import {
|
|
@@ -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,7 +902,18 @@ 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, SqlColumnIR>;
|
|
910
|
+
primaryKey?: PrimaryKey;
|
|
911
|
+
foreignKeys: readonly SqlForeignKeyIR[];
|
|
912
|
+
uniques: readonly SqlUniqueIR[];
|
|
913
|
+
indexes: readonly SqlIndexIR[];
|
|
914
|
+
checks?: SqlCheckConstraintIRInput[];
|
|
915
|
+
}
|
|
916
|
+
> = {};
|
|
899
917
|
|
|
900
918
|
for (const tableRow of tablesResult.rows) {
|
|
901
919
|
const tableName = tableRow.table_name;
|
|
@@ -927,11 +945,22 @@ 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
|
+
|
|
930
958
|
columns[colRow.column_name] = {
|
|
931
959
|
name: colRow.column_name,
|
|
932
960
|
nativeType,
|
|
933
961
|
nullable: colRow.is_nullable === 'YES',
|
|
934
962
|
...ifDefined('default', colRow.column_default ?? undefined),
|
|
963
|
+
...ifDefined('many', many),
|
|
935
964
|
};
|
|
936
965
|
}
|
|
937
966
|
|
|
@@ -1069,7 +1098,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1069
1098
|
}
|
|
1070
1099
|
}
|
|
1071
1100
|
|
|
1072
|
-
|
|
1101
|
+
tableInputs[tableName] = {
|
|
1073
1102
|
name: tableName,
|
|
1074
1103
|
columns,
|
|
1075
1104
|
...ifDefined('primaryKey', primaryKey),
|
|
@@ -1090,19 +1119,61 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1090
1119
|
[schema],
|
|
1091
1120
|
);
|
|
1092
1121
|
const nativeEnumTypeNames = nativeEnumResult.rows.map((r) => r.typname);
|
|
1122
|
+
const policiesResult = await driver.query<{
|
|
1123
|
+
schemaname: string;
|
|
1124
|
+
tablename: string;
|
|
1125
|
+
policyname: string;
|
|
1126
|
+
cmd: string;
|
|
1127
|
+
roles: string[];
|
|
1128
|
+
qual: string | null;
|
|
1129
|
+
with_check: string | null;
|
|
1130
|
+
permissive: string;
|
|
1131
|
+
}>(
|
|
1132
|
+
`SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
|
|
1133
|
+
FROM pg_catalog.pg_policies
|
|
1134
|
+
WHERE schemaname = $1
|
|
1135
|
+
ORDER BY tablename, policyname`,
|
|
1136
|
+
[schema],
|
|
1137
|
+
);
|
|
1138
|
+
const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();
|
|
1139
|
+
for (const row of policiesResult.rows) {
|
|
1140
|
+
const operation = mapPgCmd(row.cmd);
|
|
1141
|
+
const policyRoles = [
|
|
1142
|
+
...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
|
|
1143
|
+
].sort();
|
|
1144
|
+
const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
|
|
1145
|
+
const hashSuffixMatch = /^(.+)_([0-9a-f]{8})$/.exec(row.policyname);
|
|
1146
|
+
const prefix = hashSuffixMatch?.[1] ?? row.policyname;
|
|
1147
|
+
const policy = new PostgresPolicySchemaNode({
|
|
1148
|
+
name: row.policyname,
|
|
1149
|
+
prefix,
|
|
1150
|
+
tableName: row.tablename,
|
|
1151
|
+
namespaceId: row.schemaname,
|
|
1152
|
+
operation,
|
|
1153
|
+
roles: policyRoles,
|
|
1154
|
+
...(row.qual !== null ? { using: row.qual } : {}),
|
|
1155
|
+
...(row.with_check !== null ? { withCheck: row.with_check } : {}),
|
|
1156
|
+
permissive,
|
|
1157
|
+
});
|
|
1158
|
+
const list = policiesByTable.get(row.tablename) ?? [];
|
|
1159
|
+
list.push(policy);
|
|
1160
|
+
policiesByTable.set(row.tablename, list);
|
|
1161
|
+
}
|
|
1093
1162
|
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1163
|
+
const tables: Record<string, PostgresTableSchemaNode> = {};
|
|
1164
|
+
for (const [tableName, input] of Object.entries(tableInputs)) {
|
|
1165
|
+
tables[tableName] = new PostgresTableSchemaNode({
|
|
1166
|
+
...input,
|
|
1167
|
+
policies: policiesByTable.get(tableName) ?? [],
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1101
1170
|
|
|
1102
|
-
|
|
1171
|
+
const namespace = new PostgresNamespaceSchemaNode({
|
|
1172
|
+
schemaName: schema,
|
|
1103
1173
|
tables,
|
|
1104
|
-
|
|
1105
|
-
};
|
|
1174
|
+
nativeEnumTypeNames,
|
|
1175
|
+
});
|
|
1176
|
+
return { namespace, pgVersion: await this.getPostgresVersion(driver) };
|
|
1106
1177
|
}
|
|
1107
1178
|
|
|
1108
1179
|
/**
|
|
@@ -1117,6 +1188,52 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1117
1188
|
}
|
|
1118
1189
|
}
|
|
1119
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Normalises a `name[]` column value from `pg_policies.roles`.
|
|
1193
|
+
*
|
|
1194
|
+
* The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
|
|
1195
|
+
* `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
|
|
1196
|
+
* literal (`{role1,role2}`) is returned as a string instead of a JS array.
|
|
1197
|
+
* This function accepts either form and returns a plain string array.
|
|
1198
|
+
*/
|
|
1199
|
+
function parsePgNameArray(value: unknown): string[] {
|
|
1200
|
+
if (Array.isArray(value)) {
|
|
1201
|
+
return value as string[];
|
|
1202
|
+
}
|
|
1203
|
+
if (typeof value !== 'string') {
|
|
1204
|
+
return [];
|
|
1205
|
+
}
|
|
1206
|
+
const trimmed = value.trim();
|
|
1207
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
1208
|
+
return [];
|
|
1209
|
+
}
|
|
1210
|
+
const inner = trimmed.slice(1, -1);
|
|
1211
|
+
if (inner === '') {
|
|
1212
|
+
return [];
|
|
1213
|
+
}
|
|
1214
|
+
return inner.split(',').map((s) => s.trim());
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
|
|
1219
|
+
* The `pg_policies` view renders the internal command code as an uppercase
|
|
1220
|
+
* English keyword; this function lowercases to match the IR type.
|
|
1221
|
+
*/
|
|
1222
|
+
function mapPgCmd(cmd: string): RlsPolicyOperation {
|
|
1223
|
+
switch (cmd.toUpperCase()) {
|
|
1224
|
+
case 'SELECT':
|
|
1225
|
+
return 'select';
|
|
1226
|
+
case 'INSERT':
|
|
1227
|
+
return 'insert';
|
|
1228
|
+
case 'UPDATE':
|
|
1229
|
+
return 'update';
|
|
1230
|
+
case 'DELETE':
|
|
1231
|
+
return 'delete';
|
|
1232
|
+
default:
|
|
1233
|
+
return 'all';
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1120
1237
|
/**
|
|
1121
1238
|
* Extracts the namespace coordinate ids declared on a contract's storage,
|
|
1122
1239
|
* or returns an empty array when no contract (or no storage / namespaces)
|
|
@@ -1133,6 +1250,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
|
|
|
1133
1250
|
}
|
|
1134
1251
|
|
|
1135
1252
|
function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
|
|
1253
|
+
if (formattedType.endsWith('[]')) {
|
|
1254
|
+
return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
|
|
1255
|
+
}
|
|
1136
1256
|
if (formattedType === 'integer') {
|
|
1137
1257
|
return 'int4';
|
|
1138
1258
|
}
|
|
@@ -1376,6 +1496,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
|
|
|
1376
1496
|
);
|
|
1377
1497
|
}
|
|
1378
1498
|
|
|
1499
|
+
function pgRenderArrayElement(el: unknown): string {
|
|
1500
|
+
if (el === null) return 'NULL';
|
|
1501
|
+
if (typeof el === 'number' || typeof el === 'boolean') return String(el);
|
|
1502
|
+
if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
|
|
1503
|
+
return `'${escapeLiteral(JSON.stringify(el))}'`;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function pgRenderArrayLiteral(elements: unknown[]): string {
|
|
1507
|
+
if (elements.length === 0) return "'{}'";
|
|
1508
|
+
return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1379
1511
|
function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
1380
1512
|
if (wire === null) return 'NULL';
|
|
1381
1513
|
if (typeof wire === 'boolean') return wire ? 'true' : 'false';
|
|
@@ -1407,6 +1539,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
|
1407
1539
|
.join('');
|
|
1408
1540
|
return `'\\x${hex}'::${nativeType}`;
|
|
1409
1541
|
}
|
|
1542
|
+
if (Array.isArray(wire) && nativeType.endsWith('[]')) {
|
|
1543
|
+
return pgRenderArrayLiteral(wire);
|
|
1544
|
+
}
|
|
1410
1545
|
if (typeof wire === 'object') {
|
|
1411
1546
|
const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
|
|
1412
1547
|
return `${quoted}::${nativeType}`;
|
|
@@ -1477,6 +1612,9 @@ function pgRenderDdlConstraint(constraint: DdlTableConstraint): string {
|
|
|
1477
1612
|
}
|
|
1478
1613
|
return sql;
|
|
1479
1614
|
}
|
|
1615
|
+
if (constraint.kind === 'check-expression') {
|
|
1616
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} CHECK (${constraint.expression})`;
|
|
1617
|
+
}
|
|
1480
1618
|
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
1481
1619
|
if (constraint.name !== undefined) {
|
|
1482
1620
|
return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
|
|
@@ -1524,6 +1662,9 @@ async function pgRenderAlterTable(
|
|
|
1524
1662
|
const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
|
|
1525
1663
|
return `ADD COLUMN ${colFragment}`;
|
|
1526
1664
|
},
|
|
1665
|
+
dropDefault(action: DropDefaultAction): Promise<string> {
|
|
1666
|
+
return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
|
|
1667
|
+
},
|
|
1527
1668
|
};
|
|
1528
1669
|
const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
|
|
1529
1670
|
return {
|
|
@@ -1532,6 +1673,37 @@ async function pgRenderAlterTable(
|
|
|
1532
1673
|
};
|
|
1533
1674
|
}
|
|
1534
1675
|
|
|
1676
|
+
const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
|
|
1677
|
+
select: 'SELECT',
|
|
1678
|
+
insert: 'INSERT',
|
|
1679
|
+
update: 'UPDATE',
|
|
1680
|
+
delete: 'DELETE',
|
|
1681
|
+
all: 'ALL',
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
|
|
1685
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1686
|
+
const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
|
|
1687
|
+
const command = POLICY_OPERATION_SQL[node.operation];
|
|
1688
|
+
const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
|
|
1689
|
+
let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
|
|
1690
|
+
if (node.using !== undefined) {
|
|
1691
|
+
sql += ` USING (${node.using})`;
|
|
1692
|
+
}
|
|
1693
|
+
if (node.withCheck !== undefined) {
|
|
1694
|
+
sql += ` WITH CHECK (${node.withCheck})`;
|
|
1695
|
+
}
|
|
1696
|
+
return { sql, params: [] };
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
|
|
1700
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1701
|
+
return {
|
|
1702
|
+
sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
|
|
1703
|
+
params: [],
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1535
1707
|
async function pgRenderDdlExecuteRequest(
|
|
1536
1708
|
ast: PostgresDdlNode,
|
|
1537
1709
|
codecLookup: CodecLookup,
|
|
@@ -1540,6 +1712,8 @@ async function pgRenderDdlExecuteRequest(
|
|
|
1540
1712
|
createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
|
|
1541
1713
|
createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
|
|
1542
1714
|
alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
|
|
1715
|
+
createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
|
|
1716
|
+
dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
|
|
1543
1717
|
};
|
|
1544
1718
|
return ast.accept(visitor);
|
|
1545
1719
|
}
|
package/src/core/sql-renderer.ts
CHANGED
|
@@ -73,6 +73,7 @@ function renderTypedParam(
|
|
|
73
73
|
index: number,
|
|
74
74
|
codecId: string | undefined,
|
|
75
75
|
codecLookup: CodecLookup,
|
|
76
|
+
many?: boolean,
|
|
76
77
|
): string {
|
|
77
78
|
if (codecId === undefined) {
|
|
78
79
|
return `$${index}`;
|
|
@@ -97,8 +98,14 @@ function renderTypedParam(
|
|
|
97
98
|
const sqlBlock = isRecord(dbRecord) ? dbRecord['sql'] : undefined;
|
|
98
99
|
const dialectBlock = isRecord(sqlBlock) ? sqlBlock['postgres'] : undefined;
|
|
99
100
|
const nativeType = isRecord(dialectBlock) ? dialectBlock['nativeType'] : undefined;
|
|
100
|
-
if (typeof nativeType === 'string'
|
|
101
|
-
|
|
101
|
+
if (typeof nativeType === 'string') {
|
|
102
|
+
const arraySuffix = many ? '[]' : '';
|
|
103
|
+
if (!POSTGRES_INFERRABLE_NATIVE_TYPES.has(nativeType)) {
|
|
104
|
+
return `$${index}::${nativeType}${arraySuffix}`;
|
|
105
|
+
}
|
|
106
|
+
if (many) {
|
|
107
|
+
return `$${index}::${nativeType}${arraySuffix}`;
|
|
108
|
+
}
|
|
102
109
|
}
|
|
103
110
|
return `$${index}`;
|
|
104
111
|
}
|
|
@@ -736,7 +743,7 @@ function renderParamRef(ref: AnyParamRef, pim: ParamIndexMap): string {
|
|
|
736
743
|
throw new Error('ParamRef not found in index map');
|
|
737
744
|
}
|
|
738
745
|
if (ref.kind === 'prepared-param-ref') {
|
|
739
|
-
return renderTypedParam(index, ref.codec.codecId, pim.codecLookup);
|
|
746
|
+
return renderTypedParam(index, ref.codec.codecId, pim.codecLookup, ref.codec.many);
|
|
740
747
|
}
|
|
741
748
|
if (ref.codec === undefined) {
|
|
742
749
|
throw runtimeError(
|
|
@@ -747,7 +754,7 @@ function renderParamRef(ref: AnyParamRef, pim: ParamIndexMap): string {
|
|
|
747
754
|
{ paramIndex: index, ...ifDefined('name', ref.name) },
|
|
748
755
|
);
|
|
749
756
|
}
|
|
750
|
-
return renderTypedParam(index, ref.codec.codecId, pim.codecLookup);
|
|
757
|
+
return renderTypedParam(index, ref.codec.codecId, pim.codecLookup, ref.codec.many);
|
|
751
758
|
}
|
|
752
759
|
|
|
753
760
|
function renderLiteral(expr: LiteralExpr): string {
|
package/src/core/types.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Contract } from '@prisma-next/contract/types';
|
|
2
1
|
import type { CodecRegistry } from '@prisma-next/framework-components/codec';
|
|
3
|
-
import type {
|
|
2
|
+
import type { StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
|
|
4
3
|
import type {
|
|
5
4
|
AnyQueryAst,
|
|
6
5
|
BinaryExpr,
|
|
@@ -32,7 +31,7 @@ export interface PostgresAdapterOptions {
|
|
|
32
31
|
readonly codecLookup?: CodecRegistry;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
export type PostgresContract
|
|
34
|
+
export type { PostgresContract } from '@prisma-next/target-postgres/types';
|
|
36
35
|
|
|
37
36
|
export type Expr = ColumnRef | ParamRef | DefaultValueExpr;
|
|
38
37
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-CwkcdpM_.mjs","names":[],"sources":["../src/core/adapter.ts"],"sourcesContent":["import type { CodecRegistry } from '@prisma-next/framework-components/codec';\nimport { APP_SPACE_ID } from '@prisma-next/framework-components/control';\nimport type {\n Adapter,\n AdapterProfile,\n AnyQueryAst,\n LowererContext,\n RawSqlLiteral,\n SqlQueryable,\n} from '@prisma-next/sql-relational-core/ast';\nimport { isDdlNode } from '@prisma-next/sql-relational-core/ast';\nimport type { RawCodecInferer } from '@prisma-next/sql-relational-core/expression';\nimport type { PostgresDdlNode } from '@prisma-next/target-postgres/ddl';\nimport { createPostgresBuiltinCodecLookup } from './codec-lookup';\nimport { PostgresControlAdapter } from './control-adapter';\nimport { renderLoweredSql } from './sql-renderer';\nimport type { PostgresAdapterOptions, PostgresContract, PostgresLoweredStatement } from './types';\n\nconst defaultCapabilities = Object.freeze({\n postgres: {\n orderBy: true,\n limit: true,\n lateral: true,\n jsonAgg: true,\n returning: true,\n distinctOn: true,\n },\n sql: {\n enums: true,\n returning: true,\n defaultInInsert: true,\n lateral: true,\n },\n});\n\nclass PostgresAdapterImpl\n implements Adapter<AnyQueryAst, PostgresContract, PostgresLoweredStatement>\n{\n // These fields make the adapter instance structurally compatible with RuntimeAdapterInstance<'sql', 'postgres'> without introducing a runtime-plane dependency.\n readonly familyId = 'sql' as const;\n readonly targetId = 'postgres' as const;\n\n readonly profile: AdapterProfile<'postgres'>;\n private readonly codecLookup: CodecRegistry;\n\n constructor(options?: PostgresAdapterOptions) {\n this.codecLookup = options?.codecLookup ?? createPostgresBuiltinCodecLookup();\n const controlAdapter = new PostgresControlAdapter(this.codecLookup);\n this.profile = Object.freeze({\n id: options?.profileId ?? 'postgres/default@1',\n target: 'postgres',\n capabilities: defaultCapabilities,\n readMarker: (queryable: SqlQueryable) =>\n controlAdapter.readMarkerDiscriminated(\n {\n familyId: 'sql',\n targetId: 'postgres',\n query: async <Row = Record<string, unknown>>(\n sql: string,\n params?: readonly unknown[],\n ) => {\n const result = await queryable.query<Row>(sql, params);\n return { rows: [...result.rows] };\n },\n close: async () => {},\n },\n APP_SPACE_ID,\n ),\n });\n }\n\n lower(\n ast: AnyQueryAst | PostgresDdlNode,\n context: LowererContext<PostgresContract>,\n ): PostgresLoweredStatement {\n if (isDdlNode(ast)) {\n throw new Error(\n 'lower() does not lower DDL on the runtime adapter — DDL lowering is a control-plane concern handled by the control adapter.',\n );\n }\n return renderLoweredSql(ast, context.contract, this.codecLookup);\n }\n}\n\n/** Codec-id lookup for bare-literal interpolations used by `fns.raw` on a postgres client. Contributed as the descriptor's static `rawCodecInferer` slot. */\nexport const postgresRawCodecInferer: RawCodecInferer = {\n inferCodec(value: RawSqlLiteral): string {\n switch (typeof value) {\n case 'number':\n return Number.isSafeInteger(value) && value % 1 === 0 ? 'pg/int4' : 'pg/float8';\n case 'bigint':\n return 'pg/int8';\n case 'string':\n return 'pg/text';\n case 'boolean':\n return 'pg/bool';\n case 'object':\n if (value instanceof Uint8Array) return 'pg/bytea';\n }\n throw new Error(\n 'unsupported JS value type for raw-SQL interpolation: wrap this value in `param(...)` with an explicit codec',\n );\n },\n};\n\nexport function createPostgresAdapter(options?: PostgresAdapterOptions) {\n return Object.freeze(new PostgresAdapterImpl(options));\n}\n"],"mappings":";;;;AAkBA,MAAM,sBAAsB,OAAO,OAAO;CACxC,UAAU;EACR,SAAS;EACT,OAAO;EACP,SAAS;EACT,SAAS;EACT,WAAW;EACX,YAAY;CACd;CACA,KAAK;EACH,OAAO;EACP,WAAW;EACX,iBAAiB;EACjB,SAAS;CACX;AACF,CAAC;AAED,IAAM,sBAAN,MAEA;CAEE,WAAoB;CACpB,WAAoB;CAEpB;CACA;CAEA,YAAY,SAAkC;EAC5C,KAAK,cAAc,SAAS,eAAe,iCAAiC;EAC5E,MAAM,iBAAiB,IAAI,uBAAuB,KAAK,WAAW;EAClE,KAAK,UAAU,OAAO,OAAO;GAC3B,IAAI,SAAS,aAAa;GAC1B,QAAQ;GACR,cAAc;GACd,aAAa,cACX,eAAe,wBACb;IACE,UAAU;IACV,UAAU;IACV,OAAO,OACL,KACA,WACG;KAEH,OAAO,EAAE,MAAM,CAAC,IAAG,MADE,UAAU,MAAW,KAAK,MAAM,EAAA,CAC3B,IAAI,EAAE;IAClC;IACA,OAAO,YAAY,CAAC;GACtB,GACA,YACF;EACJ,CAAC;CACH;CAEA,MACE,KACA,SAC0B;EAC1B,IAAI,UAAU,GAAG,GACf,MAAM,IAAI,MACR,6HACF;EAEF,OAAO,iBAAiB,KAAK,QAAQ,UAAU,KAAK,WAAW;CACjE;AACF;;AAGA,MAAa,0BAA2C,EACtD,WAAW,OAA8B;CACvC,QAAQ,OAAO,OAAf;EACE,KAAK,UACH,OAAO,OAAO,cAAc,KAAK,KAAK,QAAQ,MAAM,IAAI,YAAY;EACtE,KAAK,UACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,UACH,IAAI,iBAAiB,YAAY,OAAO;CAC5C;CACA,MAAM,IAAI,MACR,6GACF;AACF,EACF;AAEA,SAAgB,sBAAsB,SAAkC;CACtE,OAAO,OAAO,OAAO,IAAI,oBAAoB,OAAO,CAAC;AACvD"}
|