@prisma-next/adapter-postgres 0.14.0-dev.3 → 0.14.0-dev.30
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-C9_442Ul.mjs} +4 -3
- package/dist/adapter-C9_442Ul.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-QeICiKLE.mjs} +170 -40
- package/dist/control-adapter-QeICiKLE.mjs.map +1 -0
- package/dist/control.d.mts +6 -3
- 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 +261 -45
- 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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ContractMarkerRecord,
|
|
4
|
+
LedgerEntryRecord,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
2
6
|
import {
|
|
3
7
|
parseMarkerRowSafely,
|
|
4
8
|
rethrowMarkerReadError,
|
|
@@ -7,11 +11,11 @@ import {
|
|
|
7
11
|
import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
|
|
8
12
|
import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
|
|
9
13
|
import type { CodecLookup, CodecRegistry } from '@prisma-next/framework-components/codec';
|
|
10
|
-
import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
|
|
14
|
+
import { APP_SPACE_ID, type SchemaDiffIssue } from '@prisma-next/framework-components/control';
|
|
11
15
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
12
16
|
import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
|
|
13
17
|
import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
|
|
14
|
-
import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
|
|
18
|
+
import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
15
19
|
import type {
|
|
16
20
|
AnyQueryAst,
|
|
17
21
|
CodecRef,
|
|
@@ -35,7 +39,6 @@ import type {
|
|
|
35
39
|
SqlIndexIR,
|
|
36
40
|
SqlReferentialAction,
|
|
37
41
|
SqlSchemaIR,
|
|
38
|
-
SqlTableIR,
|
|
39
42
|
SqlUniqueIR,
|
|
40
43
|
} from '@prisma-next/sql-schema-ir/types';
|
|
41
44
|
import {
|
|
@@ -45,14 +48,31 @@ import {
|
|
|
45
48
|
import type {
|
|
46
49
|
AddColumnAction,
|
|
47
50
|
AlterTableActionVisitor,
|
|
51
|
+
DropDefaultAction,
|
|
48
52
|
PostgresAlterTable,
|
|
53
|
+
PostgresCreatePolicy,
|
|
49
54
|
PostgresCreateSchema,
|
|
50
55
|
PostgresCreateTable,
|
|
51
56
|
PostgresDdlNode,
|
|
57
|
+
PostgresDropPolicy,
|
|
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 {
|
|
63
|
+
contractToPostgresSchemaIR,
|
|
64
|
+
diffPostgresSchema,
|
|
65
|
+
filterIssuesByOwnership,
|
|
66
|
+
} from '@prisma-next/target-postgres/planner';
|
|
55
67
|
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
68
|
+
import {
|
|
69
|
+
ensurePostgresSchemaIR,
|
|
70
|
+
isPostgresSchemaIR,
|
|
71
|
+
PostgresRlsPolicy,
|
|
72
|
+
PostgresRole,
|
|
73
|
+
PostgresSchemaIR,
|
|
74
|
+
PostgresTableIR,
|
|
75
|
+
} from '@prisma-next/target-postgres/types';
|
|
56
76
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
57
77
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
58
78
|
import { encodeControlQueryParams } from './control-codecs';
|
|
@@ -108,6 +128,31 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
108
128
|
*/
|
|
109
129
|
readonly normalizeNativeType = normalizeSchemaNativeType;
|
|
110
130
|
|
|
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
|
+
|
|
111
156
|
bootstrapControlTableQueries(): readonly DdlNode[] {
|
|
112
157
|
return buildControlTableBootstrapQueries();
|
|
113
158
|
}
|
|
@@ -548,26 +593,21 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
548
593
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
549
594
|
contract?: unknown,
|
|
550
595
|
schema = 'public',
|
|
551
|
-
): Promise<
|
|
596
|
+
): Promise<PostgresSchemaIR> {
|
|
552
597
|
const declaredNamespaces = extractContractNamespaceIds(contract);
|
|
553
598
|
const ir =
|
|
554
599
|
declaredNamespaces.length > 0
|
|
555
600
|
? await this.introspectNamespaces(driver, declaredNamespaces)
|
|
556
601
|
: 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.
|
|
561
602
|
const existingSchemas = await this.listExistingSchemas(driver);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
};
|
|
603
|
+
return new PostgresSchemaIR({
|
|
604
|
+
tables: ir.tables,
|
|
605
|
+
pgSchemaName: ir.pgSchemaName,
|
|
606
|
+
pgVersion: ir.pgVersion,
|
|
607
|
+
roles: ir.roles,
|
|
608
|
+
existingSchemas,
|
|
609
|
+
nativeEnumTypeNames: ir.nativeEnumTypeNames,
|
|
610
|
+
});
|
|
571
611
|
}
|
|
572
612
|
|
|
573
613
|
/**
|
|
@@ -602,7 +642,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
602
642
|
private async introspectNamespaces(
|
|
603
643
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
604
644
|
namespaceIds: readonly string[],
|
|
605
|
-
): Promise<
|
|
645
|
+
): Promise<PostgresSchemaIR> {
|
|
606
646
|
const resolvedSchemas: string[] = [];
|
|
607
647
|
for (const id of namespaceIds) {
|
|
608
648
|
if (id === UNBOUND_NAMESPACE_ID) {
|
|
@@ -619,30 +659,29 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
619
659
|
// Walk schemas sequentially: every introspectSchema call shares the one
|
|
620
660
|
// control connection, so a parallel walk only serialises behind the wire
|
|
621
661
|
// protocol and trips pg's "already executing a query" deprecation.
|
|
622
|
-
const perSchema:
|
|
662
|
+
const perSchema: PostgresSchemaIR[] = [];
|
|
623
663
|
for (const schema of uniqueSchemas) {
|
|
624
664
|
perSchema.push(await this.introspectSchema(driver, schema));
|
|
625
665
|
}
|
|
626
666
|
|
|
627
|
-
const mergedTables: Record<string,
|
|
667
|
+
const mergedTables: Record<string, PostgresTableIR> = {};
|
|
668
|
+
const mergedRoles: PostgresRole[] = [];
|
|
628
669
|
for (const ir of perSchema) {
|
|
629
670
|
for (const [tableName, table] of Object.entries(ir.tables)) {
|
|
630
671
|
mergedTables[tableName] = table;
|
|
631
672
|
}
|
|
673
|
+
mergedRoles.push(...ir.roles);
|
|
632
674
|
}
|
|
633
675
|
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
blindCast<Record<string, unknown> | undefined, 'pg annotation envelope index slot'>(
|
|
637
|
-
firstAnnotations?.['pg'],
|
|
638
|
-
) ?? {};
|
|
639
|
-
return {
|
|
676
|
+
const first = perSchema[0];
|
|
677
|
+
return new PostgresSchemaIR({
|
|
640
678
|
tables: mergedTables,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
679
|
+
pgSchemaName: first?.pgSchemaName ?? 'public',
|
|
680
|
+
pgVersion: first?.pgVersion ?? 'unknown',
|
|
681
|
+
roles: mergedRoles,
|
|
682
|
+
existingSchemas: first?.existingSchemas ?? ['public'],
|
|
683
|
+
nativeEnumTypeNames: first?.nativeEnumTypeNames ?? [],
|
|
684
|
+
});
|
|
646
685
|
}
|
|
647
686
|
|
|
648
687
|
/**
|
|
@@ -653,7 +692,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
653
692
|
private async introspectSchema(
|
|
654
693
|
driver: SqlControlDriverInstance<'postgres'>,
|
|
655
694
|
schema: string,
|
|
656
|
-
): Promise<
|
|
695
|
+
): Promise<PostgresSchemaIR> {
|
|
657
696
|
// Issue the schema-wide queries one at a time. A single control connection
|
|
658
697
|
// serialises queries anyway, so Promise.all buys no parallelism here and
|
|
659
698
|
// makes pg emit a "client is already executing a query" deprecation. One
|
|
@@ -895,7 +934,18 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
895
934
|
constraints.add(row.constraint_name);
|
|
896
935
|
}
|
|
897
936
|
|
|
898
|
-
const
|
|
937
|
+
const tableInputs: Record<
|
|
938
|
+
string,
|
|
939
|
+
{
|
|
940
|
+
name: string;
|
|
941
|
+
columns: Record<string, SqlColumnIR>;
|
|
942
|
+
primaryKey?: PrimaryKey;
|
|
943
|
+
foreignKeys: readonly SqlForeignKeyIR[];
|
|
944
|
+
uniques: readonly SqlUniqueIR[];
|
|
945
|
+
indexes: readonly SqlIndexIR[];
|
|
946
|
+
checks?: SqlCheckConstraintIRInput[];
|
|
947
|
+
}
|
|
948
|
+
> = {};
|
|
899
949
|
|
|
900
950
|
for (const tableRow of tablesResult.rows) {
|
|
901
951
|
const tableName = tableRow.table_name;
|
|
@@ -927,11 +977,22 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
927
977
|
nativeType = colRow.udt_name || colRow.data_type;
|
|
928
978
|
}
|
|
929
979
|
|
|
980
|
+
// Postgres reports array columns as data_type='ARRAY'; the element type
|
|
981
|
+
// is the `nativeType` string minus the trailing `[]`. Strip the suffix,
|
|
982
|
+
// normalize the element type to the canonical form (e.g. `integer` →
|
|
983
|
+
// `int4`), and record `many: true` so introspection consumers (verifier,
|
|
984
|
+
// psl-contract-infer) can reconstruct the full array type as needed.
|
|
985
|
+
const many = nativeType.endsWith('[]') ? true : undefined;
|
|
986
|
+
if (many) {
|
|
987
|
+
nativeType = normalizeSchemaNativeType(nativeType.slice(0, -2));
|
|
988
|
+
}
|
|
989
|
+
|
|
930
990
|
columns[colRow.column_name] = {
|
|
931
991
|
name: colRow.column_name,
|
|
932
992
|
nativeType,
|
|
933
993
|
nullable: colRow.is_nullable === 'YES',
|
|
934
994
|
...ifDefined('default', colRow.column_default ?? undefined),
|
|
995
|
+
...ifDefined('many', many),
|
|
935
996
|
};
|
|
936
997
|
}
|
|
937
998
|
|
|
@@ -1069,7 +1130,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1069
1130
|
}
|
|
1070
1131
|
}
|
|
1071
1132
|
|
|
1072
|
-
|
|
1133
|
+
tableInputs[tableName] = {
|
|
1073
1134
|
name: tableName,
|
|
1074
1135
|
columns,
|
|
1075
1136
|
...ifDefined('primaryKey', primaryKey),
|
|
@@ -1090,19 +1151,74 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1090
1151
|
[schema],
|
|
1091
1152
|
);
|
|
1092
1153
|
const nativeEnumTypeNames = nativeEnumResult.rows.map((r) => r.typname);
|
|
1154
|
+
const policiesResult = await driver.query<{
|
|
1155
|
+
schemaname: string;
|
|
1156
|
+
tablename: string;
|
|
1157
|
+
policyname: string;
|
|
1158
|
+
cmd: string;
|
|
1159
|
+
roles: string[];
|
|
1160
|
+
qual: string | null;
|
|
1161
|
+
with_check: string | null;
|
|
1162
|
+
permissive: string;
|
|
1163
|
+
}>(
|
|
1164
|
+
`SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check, permissive
|
|
1165
|
+
FROM pg_catalog.pg_policies
|
|
1166
|
+
WHERE schemaname = $1
|
|
1167
|
+
ORDER BY tablename, policyname`,
|
|
1168
|
+
[schema],
|
|
1169
|
+
);
|
|
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[]>();
|
|
1178
|
+
for (const row of policiesResult.rows) {
|
|
1179
|
+
const operation = mapPgCmd(row.cmd);
|
|
1180
|
+
const policyRoles = [
|
|
1181
|
+
...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
|
|
1182
|
+
].sort();
|
|
1183
|
+
const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
|
|
1184
|
+
const hashSuffixMatch = /^(.+)_([0-9a-f]{8})$/.exec(row.policyname);
|
|
1185
|
+
const prefix = hashSuffixMatch?.[1] ?? row.policyname;
|
|
1186
|
+
const policy = new PostgresRlsPolicy({
|
|
1187
|
+
name: row.policyname,
|
|
1188
|
+
prefix,
|
|
1189
|
+
tableName: row.tablename,
|
|
1190
|
+
namespaceId: row.schemaname,
|
|
1191
|
+
operation,
|
|
1192
|
+
roles: policyRoles,
|
|
1193
|
+
...(row.qual !== null ? { using: row.qual } : {}),
|
|
1194
|
+
...(row.with_check !== null ? { withCheck: row.with_check } : {}),
|
|
1195
|
+
permissive,
|
|
1196
|
+
});
|
|
1197
|
+
const list = policiesByTable.get(row.tablename) ?? [];
|
|
1198
|
+
list.push(policy);
|
|
1199
|
+
policiesByTable.set(row.tablename, list);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const tables: Record<string, PostgresTableIR> = {};
|
|
1203
|
+
for (const [tableName, input] of Object.entries(tableInputs)) {
|
|
1204
|
+
tables[tableName] = new PostgresTableIR({
|
|
1205
|
+
...input,
|
|
1206
|
+
rlsPolicies: policiesByTable.get(tableName) ?? [],
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1093
1209
|
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
version: await this.getPostgresVersion(driver),
|
|
1098
|
-
...(nativeEnumTypeNames.length > 0 && { nativeEnumTypeNames }),
|
|
1099
|
-
},
|
|
1100
|
-
};
|
|
1210
|
+
const roles: PostgresRole[] = rolesResult.rows.map(
|
|
1211
|
+
(row) => new PostgresRole({ name: row.rolname, namespaceId: UNBOUND_NAMESPACE_ID }),
|
|
1212
|
+
);
|
|
1101
1213
|
|
|
1102
|
-
return {
|
|
1214
|
+
return new PostgresSchemaIR({
|
|
1103
1215
|
tables,
|
|
1104
|
-
|
|
1105
|
-
|
|
1216
|
+
pgSchemaName: schema,
|
|
1217
|
+
pgVersion: await this.getPostgresVersion(driver),
|
|
1218
|
+
roles,
|
|
1219
|
+
existingSchemas: [],
|
|
1220
|
+
nativeEnumTypeNames,
|
|
1221
|
+
});
|
|
1106
1222
|
}
|
|
1107
1223
|
|
|
1108
1224
|
/**
|
|
@@ -1117,6 +1233,52 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1117
1233
|
}
|
|
1118
1234
|
}
|
|
1119
1235
|
|
|
1236
|
+
/**
|
|
1237
|
+
* Normalises a `name[]` column value from `pg_policies.roles`.
|
|
1238
|
+
*
|
|
1239
|
+
* The `pg` client's type-parser registry handles `text[]` (OID 1009) but not
|
|
1240
|
+
* `name[]` (OID 1003). When the parser is absent the raw Postgres text-array
|
|
1241
|
+
* literal (`{role1,role2}`) is returned as a string instead of a JS array.
|
|
1242
|
+
* This function accepts either form and returns a plain string array.
|
|
1243
|
+
*/
|
|
1244
|
+
function parsePgNameArray(value: unknown): string[] {
|
|
1245
|
+
if (Array.isArray(value)) {
|
|
1246
|
+
return value as string[];
|
|
1247
|
+
}
|
|
1248
|
+
if (typeof value !== 'string') {
|
|
1249
|
+
return [];
|
|
1250
|
+
}
|
|
1251
|
+
const trimmed = value.trim();
|
|
1252
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
1253
|
+
return [];
|
|
1254
|
+
}
|
|
1255
|
+
const inner = trimmed.slice(1, -1);
|
|
1256
|
+
if (inner === '') {
|
|
1257
|
+
return [];
|
|
1258
|
+
}
|
|
1259
|
+
return inner.split(',').map((s) => s.trim());
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Maps `pg_policies.cmd` text values to the `RlsPolicyOperation` union.
|
|
1264
|
+
* The `pg_policies` view renders the internal command code as an uppercase
|
|
1265
|
+
* English keyword; this function lowercases to match the IR type.
|
|
1266
|
+
*/
|
|
1267
|
+
function mapPgCmd(cmd: string): RlsPolicyOperation {
|
|
1268
|
+
switch (cmd.toUpperCase()) {
|
|
1269
|
+
case 'SELECT':
|
|
1270
|
+
return 'select';
|
|
1271
|
+
case 'INSERT':
|
|
1272
|
+
return 'insert';
|
|
1273
|
+
case 'UPDATE':
|
|
1274
|
+
return 'update';
|
|
1275
|
+
case 'DELETE':
|
|
1276
|
+
return 'delete';
|
|
1277
|
+
default:
|
|
1278
|
+
return 'all';
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1120
1282
|
/**
|
|
1121
1283
|
* Extracts the namespace coordinate ids declared on a contract's storage,
|
|
1122
1284
|
* or returns an empty array when no contract (or no storage / namespaces)
|
|
@@ -1133,6 +1295,9 @@ function extractContractNamespaceIds(contract: unknown): readonly string[] {
|
|
|
1133
1295
|
}
|
|
1134
1296
|
|
|
1135
1297
|
function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
|
|
1298
|
+
if (formattedType.endsWith('[]')) {
|
|
1299
|
+
return `${normalizeFormattedType(formattedType.slice(0, -2), dataType, udtName)}[]`;
|
|
1300
|
+
}
|
|
1136
1301
|
if (formattedType === 'integer') {
|
|
1137
1302
|
return 'int4';
|
|
1138
1303
|
}
|
|
@@ -1376,6 +1541,18 @@ function pgIsTextLikeNativeType(nativeType: string): boolean {
|
|
|
1376
1541
|
);
|
|
1377
1542
|
}
|
|
1378
1543
|
|
|
1544
|
+
function pgRenderArrayElement(el: unknown): string {
|
|
1545
|
+
if (el === null) return 'NULL';
|
|
1546
|
+
if (typeof el === 'number' || typeof el === 'boolean') return String(el);
|
|
1547
|
+
if (typeof el === 'string') return `'${escapeLiteral(el)}'`;
|
|
1548
|
+
return `'${escapeLiteral(JSON.stringify(el))}'`;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function pgRenderArrayLiteral(elements: unknown[]): string {
|
|
1552
|
+
if (elements.length === 0) return "'{}'";
|
|
1553
|
+
return `ARRAY[${elements.map(pgRenderArrayElement).join(', ')}]`;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1379
1556
|
function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
1380
1557
|
if (wire === null) return 'NULL';
|
|
1381
1558
|
if (typeof wire === 'boolean') return wire ? 'true' : 'false';
|
|
@@ -1407,6 +1584,9 @@ function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
|
1407
1584
|
.join('');
|
|
1408
1585
|
return `'\\x${hex}'::${nativeType}`;
|
|
1409
1586
|
}
|
|
1587
|
+
if (Array.isArray(wire) && nativeType.endsWith('[]')) {
|
|
1588
|
+
return pgRenderArrayLiteral(wire);
|
|
1589
|
+
}
|
|
1410
1590
|
if (typeof wire === 'object') {
|
|
1411
1591
|
const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
|
|
1412
1592
|
return `${quoted}::${nativeType}`;
|
|
@@ -1524,6 +1704,9 @@ async function pgRenderAlterTable(
|
|
|
1524
1704
|
const colFragment = await pgRenderDdlColumn(action.column, codecLookup);
|
|
1525
1705
|
return `ADD COLUMN ${colFragment}`;
|
|
1526
1706
|
},
|
|
1707
|
+
dropDefault(action: DropDefaultAction): Promise<string> {
|
|
1708
|
+
return Promise.resolve(`ALTER COLUMN ${quoteIdentifier(action.columnName)} DROP DEFAULT`);
|
|
1709
|
+
},
|
|
1527
1710
|
};
|
|
1528
1711
|
const actionSqls = await Promise.all(node.actions.map((a) => a.accept(actionVisitor)));
|
|
1529
1712
|
return {
|
|
@@ -1532,6 +1715,37 @@ async function pgRenderAlterTable(
|
|
|
1532
1715
|
};
|
|
1533
1716
|
}
|
|
1534
1717
|
|
|
1718
|
+
const POLICY_OPERATION_SQL: Record<RlsPolicyOperation, string> = {
|
|
1719
|
+
select: 'SELECT',
|
|
1720
|
+
insert: 'INSERT',
|
|
1721
|
+
update: 'UPDATE',
|
|
1722
|
+
delete: 'DELETE',
|
|
1723
|
+
all: 'ALL',
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
function pgRenderCreatePolicy(node: PostgresCreatePolicy): SqlExecuteRequest {
|
|
1727
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1728
|
+
const permissiveness = node.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
|
|
1729
|
+
const command = POLICY_OPERATION_SQL[node.operation];
|
|
1730
|
+
const roles = node.roles.length === 0 ? 'PUBLIC' : node.roles.join(', ');
|
|
1731
|
+
let sql = `CREATE POLICY ${quoteIdentifier(node.name)} ON ${tableRef} AS ${permissiveness} FOR ${command} TO ${roles}`;
|
|
1732
|
+
if (node.using !== undefined) {
|
|
1733
|
+
sql += ` USING (${node.using})`;
|
|
1734
|
+
}
|
|
1735
|
+
if (node.withCheck !== undefined) {
|
|
1736
|
+
sql += ` WITH CHECK (${node.withCheck})`;
|
|
1737
|
+
}
|
|
1738
|
+
return { sql, params: [] };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function pgRenderDropPolicy(node: PostgresDropPolicy): SqlExecuteRequest {
|
|
1742
|
+
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
1743
|
+
return {
|
|
1744
|
+
sql: `DROP POLICY ${quoteIdentifier(node.name)} ON ${tableRef}`,
|
|
1745
|
+
params: [],
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1535
1749
|
async function pgRenderDdlExecuteRequest(
|
|
1536
1750
|
ast: PostgresDdlNode,
|
|
1537
1751
|
codecLookup: CodecLookup,
|
|
@@ -1540,6 +1754,8 @@ async function pgRenderDdlExecuteRequest(
|
|
|
1540
1754
|
createTable: (node: PostgresCreateTable) => pgRenderCreateTable(node, codecLookup),
|
|
1541
1755
|
createSchema: (node: PostgresCreateSchema) => Promise.resolve(pgRenderCreateSchema(node)),
|
|
1542
1756
|
alterTable: (node: PostgresAlterTable) => pgRenderAlterTable(node, codecLookup),
|
|
1757
|
+
createPolicy: (node: PostgresCreatePolicy) => Promise.resolve(pgRenderCreatePolicy(node)),
|
|
1758
|
+
dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
|
|
1543
1759
|
};
|
|
1544
1760
|
return ast.accept(visitor);
|
|
1545
1761
|
}
|
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"}
|