@prisma-next/adapter-postgres 0.13.0-dev.2 → 0.13.0-dev.20
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-CAlWA4ug.mjs → adapter-LFqgZuQu.mjs} +3 -3
- package/dist/adapter-LFqgZuQu.mjs.map +1 -0
- package/dist/adapter.d.mts.map +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/{control-adapter-ZWrjGBq7.mjs → control-adapter-cYV_uWx2.mjs} +202 -102
- package/dist/control-adapter-cYV_uWx2.mjs.map +1 -0
- package/dist/control.d.mts +9 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +2 -2
- package/dist/{descriptor-meta-NBwpqHS7.mjs → descriptor-meta-DOgMfoqm.mjs} +10 -3
- package/dist/descriptor-meta-DOgMfoqm.mjs.map +1 -0
- package/dist/runtime.mjs +2 -2
- package/package.json +22 -22
- package/src/core/adapter.ts +3 -2
- package/src/core/control-adapter.ts +226 -24
- package/src/core/control-codecs.ts +25 -0
- package/src/core/descriptor-meta.ts +3 -0
- package/src/core/marker-ledger.ts +2 -18
- package/src/core/sql-renderer.ts +122 -1
- package/dist/adapter-CAlWA4ug.mjs.map +0 -1
- package/dist/control-adapter-ZWrjGBq7.mjs.map +0 -1
- package/dist/descriptor-meta-NBwpqHS7.mjs.map +0 -1
- package/src/core/ddl-renderer.ts +0 -155
|
@@ -14,6 +14,7 @@ import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
|
14
14
|
import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
|
|
15
15
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
16
16
|
import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
|
|
17
|
+
import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
|
|
17
18
|
import type {
|
|
18
19
|
PostgresEnumStorageEntry,
|
|
19
20
|
SqlControlDriverInstance,
|
|
@@ -21,10 +22,16 @@ import type {
|
|
|
21
22
|
} from '@prisma-next/sql-contract/types';
|
|
22
23
|
import type {
|
|
23
24
|
AnyQueryAst,
|
|
25
|
+
CodecRef,
|
|
26
|
+
DdlColumn,
|
|
24
27
|
DdlNode,
|
|
28
|
+
DdlTableConstraint,
|
|
29
|
+
FunctionColumnDefault,
|
|
30
|
+
LiteralColumnDefault,
|
|
25
31
|
LoweredStatement,
|
|
26
32
|
LowererContext,
|
|
27
33
|
MarkerReadResult,
|
|
34
|
+
SqlExecuteRequest,
|
|
28
35
|
} from '@prisma-next/sql-relational-core/ast';
|
|
29
36
|
import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
|
|
30
37
|
import type {
|
|
@@ -42,19 +49,23 @@ import {
|
|
|
42
49
|
buildControlTableBootstrapQueries,
|
|
43
50
|
buildSignMarkerBootstrapQueries,
|
|
44
51
|
} from '@prisma-next/target-postgres/contract-free';
|
|
45
|
-
import type {
|
|
52
|
+
import type {
|
|
53
|
+
PostgresCreateSchema,
|
|
54
|
+
PostgresCreateTable,
|
|
55
|
+
PostgresDdlNode,
|
|
56
|
+
} from '@prisma-next/target-postgres/ddl';
|
|
46
57
|
import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
|
|
47
58
|
import {
|
|
48
59
|
createResolveExistingEnumValues,
|
|
49
|
-
enumStorageCompoundKey,
|
|
50
60
|
readExistingEnumValues,
|
|
51
61
|
readPostgresSchemaIrAnnotations,
|
|
52
62
|
} from '@prisma-next/target-postgres/enum-planning';
|
|
53
63
|
import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
|
|
64
|
+
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
54
65
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
55
66
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
56
67
|
import { createPostgresBuiltinCodecLookup } from './codec-lookup';
|
|
57
|
-
import {
|
|
68
|
+
import { encodeControlQueryParams } from './control-codecs';
|
|
58
69
|
import {
|
|
59
70
|
introspectPostgresEnumTypes,
|
|
60
71
|
type PostgresEnumStorageTypeAnnotation,
|
|
@@ -157,11 +168,39 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
157
168
|
*/
|
|
158
169
|
lower(ast: AnyQueryAst | PostgresDdlNode, context: LowererContext<unknown>): LoweredStatement {
|
|
159
170
|
if (isDdlNode(ast)) {
|
|
160
|
-
|
|
171
|
+
throw new Error(
|
|
172
|
+
'lower() cannot lower DDL: DDL default literals require inline codec encoding, which is async. Use lowerToExecuteRequest().',
|
|
173
|
+
);
|
|
161
174
|
}
|
|
162
175
|
return renderLoweredSql(ast, context.contract as PostgresContract, this.codecLookup);
|
|
163
176
|
}
|
|
164
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Lower an AST all the way to a driver-ready statement. For DDL nodes,
|
|
180
|
+
* literal column defaults are formatted as inline SQL with proper quoting and
|
|
181
|
+
* `::nativeType` cast suffixes. For query ASTs, params are kept as `$N`
|
|
182
|
+
* placeholders; wire values go in `params`. Does NOT call `this.lower()` —
|
|
183
|
+
* independent implementation.
|
|
184
|
+
*/
|
|
185
|
+
async lowerToExecuteRequest(
|
|
186
|
+
ast: AnyQueryAst | PostgresDdlNode,
|
|
187
|
+
context?: LowererContext<unknown>,
|
|
188
|
+
): Promise<SqlExecuteRequest> {
|
|
189
|
+
if (isDdlNode(ast)) {
|
|
190
|
+
return pgRenderDdlExecuteRequest(
|
|
191
|
+
blindCast<PostgresDdlNode, 'isDdlNode guard'>(ast),
|
|
192
|
+
this.codecLookup,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const lowered = renderLoweredSql(
|
|
196
|
+
ast,
|
|
197
|
+
blindCast<PostgresContract, 'Caller must supply matching contract'>(context?.contract),
|
|
198
|
+
this.codecLookup,
|
|
199
|
+
);
|
|
200
|
+
const params = await encodeControlQueryParams(lowered, ast);
|
|
201
|
+
return { sql: lowered.sql, params };
|
|
202
|
+
}
|
|
203
|
+
|
|
165
204
|
/**
|
|
166
205
|
* Reads the contract marker from `prisma_contract.marker`. Probes
|
|
167
206
|
* `information_schema.tables` first so a fresh database (where the
|
|
@@ -625,16 +664,18 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
625
664
|
}
|
|
626
665
|
}
|
|
627
666
|
|
|
628
|
-
const
|
|
629
|
-
for (
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
667
|
+
const mergedEnumTypes: Record<string, Record<string, PostgresEnumStorageTypeAnnotation>> = {};
|
|
668
|
+
for (const ir of perSchema) {
|
|
669
|
+
const enumTypes = blindCast<
|
|
670
|
+
| { enumTypes?: Record<string, Record<string, PostgresEnumStorageTypeAnnotation>> }
|
|
671
|
+
| undefined,
|
|
633
672
|
'pg annotation envelope index slot'
|
|
634
|
-
>(ir?.annotations?.['pg'])?.
|
|
635
|
-
if (!
|
|
636
|
-
for (const [
|
|
637
|
-
|
|
673
|
+
>(ir?.annotations?.['pg'])?.enumTypes;
|
|
674
|
+
if (!enumTypes) continue;
|
|
675
|
+
for (const [schemaName, byType] of Object.entries(enumTypes)) {
|
|
676
|
+
const merged = mergedEnumTypes[schemaName] ?? {};
|
|
677
|
+
Object.assign(merged, byType);
|
|
678
|
+
mergedEnumTypes[schemaName] = merged;
|
|
638
679
|
}
|
|
639
680
|
}
|
|
640
681
|
|
|
@@ -650,8 +691,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
650
691
|
pg: {
|
|
651
692
|
...firstPg,
|
|
652
693
|
...ifDefined(
|
|
653
|
-
'
|
|
654
|
-
Object.keys(
|
|
694
|
+
'enumTypes',
|
|
695
|
+
Object.keys(mergedEnumTypes).length > 0 ? mergedEnumTypes : undefined,
|
|
655
696
|
),
|
|
656
697
|
},
|
|
657
698
|
}),
|
|
@@ -1093,20 +1134,17 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1093
1134
|
};
|
|
1094
1135
|
}
|
|
1095
1136
|
|
|
1096
|
-
const
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1137
|
+
const rawEnumTypes = await introspectPostgresEnumTypes({ driver, schemaName: schema });
|
|
1138
|
+
const enumTypes: Record<
|
|
1139
|
+
string,
|
|
1140
|
+
Record<string, PostgresEnumStorageTypeAnnotation>
|
|
1141
|
+
> = Object.keys(rawEnumTypes).length > 0 ? { [schema]: rawEnumTypes } : {};
|
|
1101
1142
|
|
|
1102
1143
|
const annotations = {
|
|
1103
1144
|
pg: {
|
|
1104
1145
|
schema,
|
|
1105
1146
|
version: await this.getPostgresVersion(driver),
|
|
1106
|
-
...ifDefined(
|
|
1107
|
-
'storageTypes',
|
|
1108
|
-
Object.keys(storageTypes).length > 0 ? storageTypes : undefined,
|
|
1109
|
-
),
|
|
1147
|
+
...ifDefined('enumTypes', Object.keys(enumTypes).length > 0 ? enumTypes : undefined),
|
|
1110
1148
|
},
|
|
1111
1149
|
};
|
|
1112
1150
|
|
|
@@ -1368,3 +1406,167 @@ function extractQuotedLiterals(listBody: string): readonly string[] | undefined
|
|
|
1368
1406
|
const values = [...listBody.matchAll(pattern)].map((m) => (m[1] ?? '').replace(/''/g, "'"));
|
|
1369
1407
|
return values.length > 0 ? values : undefined;
|
|
1370
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
// ---------------------------------------------------------------------------
|
|
1411
|
+
// pgRenderDdlExecuteRequest — independent DDL walker for lowerToExecuteRequest
|
|
1412
|
+
// ---------------------------------------------------------------------------
|
|
1413
|
+
|
|
1414
|
+
function pgIsTextLikeNativeType(nativeType: string): boolean {
|
|
1415
|
+
return (
|
|
1416
|
+
nativeType === 'text' ||
|
|
1417
|
+
nativeType === 'varchar' ||
|
|
1418
|
+
nativeType.startsWith('varchar(') ||
|
|
1419
|
+
nativeType === 'character varying' ||
|
|
1420
|
+
nativeType.startsWith('character varying(') ||
|
|
1421
|
+
nativeType === 'char' ||
|
|
1422
|
+
nativeType.startsWith('char(') ||
|
|
1423
|
+
nativeType === 'character' ||
|
|
1424
|
+
nativeType.startsWith('character(')
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function pgInlineLiteral(wire: unknown, nativeType: string): string {
|
|
1429
|
+
if (wire === null) return 'NULL';
|
|
1430
|
+
if (typeof wire === 'boolean') return wire ? 'true' : 'false';
|
|
1431
|
+
if (typeof wire === 'number') {
|
|
1432
|
+
if (!Number.isFinite(wire)) {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
`pgRenderDdlExecuteRequest: non-finite number wire value ${String(wire)} cannot be emitted as a DEFAULT literal for native type "${nativeType}"`,
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
return String(wire);
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof wire === 'bigint') return String(wire);
|
|
1440
|
+
if (wire instanceof Date) {
|
|
1441
|
+
if (Number.isNaN(wire.getTime())) {
|
|
1442
|
+
throw new Error(
|
|
1443
|
+
`pgRenderDdlExecuteRequest: invalid Date value cannot be emitted as a DEFAULT literal for native type "${nativeType}"`,
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
const quoted = `'${escapeLiteral(wire.toISOString())}'`;
|
|
1447
|
+
return pgIsTextLikeNativeType(nativeType) ? quoted : `${quoted}::${nativeType}`;
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof wire === 'string') {
|
|
1450
|
+
const quoted = `'${escapeLiteral(wire)}'`;
|
|
1451
|
+
return pgIsTextLikeNativeType(nativeType) ? quoted : `${quoted}::${nativeType}`;
|
|
1452
|
+
}
|
|
1453
|
+
if (wire instanceof Uint8Array) {
|
|
1454
|
+
const hex = Array.from(wire)
|
|
1455
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1456
|
+
.join('');
|
|
1457
|
+
return `'\\x${hex}'::${nativeType}`;
|
|
1458
|
+
}
|
|
1459
|
+
if (typeof wire === 'object') {
|
|
1460
|
+
const quoted = `'${escapeLiteral(JSON.stringify(wire))}'`;
|
|
1461
|
+
return `${quoted}::${nativeType}`;
|
|
1462
|
+
}
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`pgRenderDdlExecuteRequest: unexpected wire type "${typeof wire}" for native type "${nativeType}"`,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async function pgRenderDdlColumnDefault(
|
|
1469
|
+
def: LiteralColumnDefault | FunctionColumnDefault,
|
|
1470
|
+
nativeType: string,
|
|
1471
|
+
codecLookup: CodecLookup,
|
|
1472
|
+
codecRef: CodecRef | undefined,
|
|
1473
|
+
): Promise<string> {
|
|
1474
|
+
if (def.kind === 'function') {
|
|
1475
|
+
if (def.expression === 'autoincrement()') return '';
|
|
1476
|
+
return `DEFAULT (${def.expression})`;
|
|
1477
|
+
}
|
|
1478
|
+
if (codecRef !== undefined) {
|
|
1479
|
+
const codec = codecLookup.get(codecRef.codecId);
|
|
1480
|
+
if (codec !== undefined) {
|
|
1481
|
+
const wire = await codec.encode(def.value, {});
|
|
1482
|
+
return `DEFAULT ${pgInlineLiteral(wire, nativeType)}`;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
// Fallback: codec-less literal defaults follow RawSqlLiteral wire-scalar semantics.
|
|
1486
|
+
return `DEFAULT ${pgInlineLiteral(def.value, nativeType)}`;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
async function pgRenderDdlColumn(column: DdlColumn, codecLookup: CodecLookup): Promise<string> {
|
|
1490
|
+
const parts = [quoteIdentifier(column.name), column.type];
|
|
1491
|
+
if (column.notNull) parts.push('NOT NULL');
|
|
1492
|
+
if (column.primaryKey) parts.push('PRIMARY KEY');
|
|
1493
|
+
if (column.default) {
|
|
1494
|
+
const clause = await pgRenderDdlColumnDefault(
|
|
1495
|
+
column.default,
|
|
1496
|
+
column.type,
|
|
1497
|
+
codecLookup,
|
|
1498
|
+
column.codecRef,
|
|
1499
|
+
);
|
|
1500
|
+
if (clause.length > 0) parts.push(clause);
|
|
1501
|
+
}
|
|
1502
|
+
return parts.join(' ');
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function pgRenderDdlConstraint(constraint: DdlTableConstraint): string {
|
|
1506
|
+
if (constraint.kind === 'primary-key') {
|
|
1507
|
+
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
1508
|
+
if (constraint.name !== undefined) {
|
|
1509
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} PRIMARY KEY (${cols})`;
|
|
1510
|
+
}
|
|
1511
|
+
return `PRIMARY KEY (${cols})`;
|
|
1512
|
+
}
|
|
1513
|
+
if (constraint.kind === 'foreign-key') {
|
|
1514
|
+
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
1515
|
+
const refTable = constraint.refTable.split('.').map(quoteIdentifier).join('.');
|
|
1516
|
+
const refCols = constraint.refColumns.map(quoteIdentifier).join(', ');
|
|
1517
|
+
let sql = `FOREIGN KEY (${cols}) REFERENCES ${refTable} (${refCols})`;
|
|
1518
|
+
if (constraint.onDelete !== undefined) {
|
|
1519
|
+
sql += ` ON DELETE ${REFERENTIAL_ACTION_SQL[constraint.onDelete]}`;
|
|
1520
|
+
}
|
|
1521
|
+
if (constraint.onUpdate !== undefined) {
|
|
1522
|
+
sql += ` ON UPDATE ${REFERENTIAL_ACTION_SQL[constraint.onUpdate]}`;
|
|
1523
|
+
}
|
|
1524
|
+
if (constraint.name !== undefined) {
|
|
1525
|
+
sql = `CONSTRAINT ${quoteIdentifier(constraint.name)} ${sql}`;
|
|
1526
|
+
}
|
|
1527
|
+
return sql;
|
|
1528
|
+
}
|
|
1529
|
+
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
1530
|
+
if (constraint.name !== undefined) {
|
|
1531
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
|
|
1532
|
+
}
|
|
1533
|
+
return `UNIQUE (${cols})`;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
async function pgRenderCreateTable(
|
|
1537
|
+
node: PostgresCreateTable,
|
|
1538
|
+
codecLookup: CodecLookup,
|
|
1539
|
+
): Promise<SqlExecuteRequest> {
|
|
1540
|
+
const ifNotExists = node.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
1541
|
+
const tableRef = node.schema
|
|
1542
|
+
? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`
|
|
1543
|
+
: quoteIdentifier(node.table);
|
|
1544
|
+
const columnDefs = await Promise.all(
|
|
1545
|
+
node.columns.map((col) => pgRenderDdlColumn(col, codecLookup)),
|
|
1546
|
+
);
|
|
1547
|
+
const constraintDefs =
|
|
1548
|
+
node.constraints !== undefined ? node.constraints.map(pgRenderDdlConstraint) : [];
|
|
1549
|
+
const allDefs = [...columnDefs, ...constraintDefs].join(',\n ');
|
|
1550
|
+
return {
|
|
1551
|
+
sql: `CREATE TABLE ${ifNotExists}${tableRef} (\n ${allDefs}\n)`,
|
|
1552
|
+
params: [],
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function pgRenderCreateSchema(node: PostgresCreateSchema): SqlExecuteRequest {
|
|
1557
|
+
const ifNotExists = node.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
1558
|
+
return {
|
|
1559
|
+
sql: `CREATE SCHEMA ${ifNotExists}${quoteIdentifier(node.schema)}`,
|
|
1560
|
+
params: [],
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async function pgRenderDdlExecuteRequest(
|
|
1565
|
+
ast: PostgresDdlNode,
|
|
1566
|
+
codecLookup: CodecLookup,
|
|
1567
|
+
): Promise<SqlExecuteRequest> {
|
|
1568
|
+
if (ast.kind === 'create-table') {
|
|
1569
|
+
return pgRenderCreateTable(blindCast<PostgresCreateTable, 'kind guard'>(ast), codecLookup);
|
|
1570
|
+
}
|
|
1571
|
+
return pgRenderCreateSchema(blindCast<PostgresCreateSchema, 'kind guard'>(ast));
|
|
1572
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyQueryAst,
|
|
3
|
+
ContractCodecRegistry,
|
|
4
|
+
LoweredStatement,
|
|
5
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
6
|
+
import {
|
|
7
|
+
createAstCodecRegistry,
|
|
8
|
+
deriveParamMetadata,
|
|
9
|
+
encodeParamsWithMetadata,
|
|
10
|
+
} from '@prisma-next/sql-runtime';
|
|
11
|
+
import { postgresCodecRegistry } from '@prisma-next/target-postgres/codecs';
|
|
12
|
+
|
|
13
|
+
export const CONTROL_CODECS = createAstCodecRegistry(postgresCodecRegistry);
|
|
14
|
+
|
|
15
|
+
export async function encodeControlQueryParams(
|
|
16
|
+
lowered: LoweredStatement,
|
|
17
|
+
ast: AnyQueryAst,
|
|
18
|
+
codecs: ContractCodecRegistry = CONTROL_CODECS,
|
|
19
|
+
): Promise<readonly unknown[]> {
|
|
20
|
+
const values = lowered.params.map((slot) => {
|
|
21
|
+
if (slot.kind === 'literal') return slot.value;
|
|
22
|
+
throw new Error(`control query lowered to a bind slot '${slot.name}', which is unsupported`);
|
|
23
|
+
});
|
|
24
|
+
return encodeParamsWithMetadata(values, deriveParamMetadata(ast), {}, codecs);
|
|
25
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
PG_TIMESTAMP_CODEC_ID,
|
|
28
28
|
PG_TIMESTAMPTZ_CODEC_ID,
|
|
29
29
|
PG_TIMETZ_CODEC_ID,
|
|
30
|
+
PG_UUID_CODEC_ID,
|
|
30
31
|
PG_VARBIT_CODEC_ID,
|
|
31
32
|
PG_VARCHAR_CODEC_ID,
|
|
32
33
|
SQL_CHAR_CODEC_ID,
|
|
@@ -215,6 +216,7 @@ export const postgresAdapterDescriptorMeta = {
|
|
|
215
216
|
[PG_JSON_CODEC_ID]: identityHooks,
|
|
216
217
|
[PG_JSONB_CODEC_ID]: identityHooks,
|
|
217
218
|
[PG_BYTEA_CODEC_ID]: identityHooks,
|
|
219
|
+
[PG_UUID_CODEC_ID]: identityHooks,
|
|
218
220
|
},
|
|
219
221
|
},
|
|
220
222
|
storage: [
|
|
@@ -281,6 +283,7 @@ export const postgresAdapterDescriptorMeta = {
|
|
|
281
283
|
{ typeId: PG_JSON_CODEC_ID, familyId: 'sql', targetId: 'postgres', nativeType: 'json' },
|
|
282
284
|
{ typeId: PG_JSONB_CODEC_ID, familyId: 'sql', targetId: 'postgres', nativeType: 'jsonb' },
|
|
283
285
|
{ typeId: PG_BYTEA_CODEC_ID, familyId: 'sql', targetId: 'postgres', nativeType: 'bytea' },
|
|
286
|
+
{ typeId: PG_UUID_CODEC_ID, familyId: 'sql', targetId: 'postgres', nativeType: 'uuid' },
|
|
284
287
|
],
|
|
285
288
|
queryOperationTypes: {
|
|
286
289
|
import: {
|
|
@@ -4,13 +4,7 @@ import {
|
|
|
4
4
|
type LoweredStatement,
|
|
5
5
|
RawExpr,
|
|
6
6
|
} from '@prisma-next/sql-relational-core/ast';
|
|
7
|
-
import {
|
|
8
|
-
createAstCodecRegistry,
|
|
9
|
-
deriveParamMetadata,
|
|
10
|
-
encodeParamsWithMetadata,
|
|
11
|
-
} from '@prisma-next/sql-runtime';
|
|
12
7
|
import { PG_TIMESTAMPTZ_CODEC_ID } from '@prisma-next/target-postgres/codec-ids';
|
|
13
|
-
import { postgresCodecRegistry } from '@prisma-next/target-postgres/codecs';
|
|
14
8
|
import {
|
|
15
9
|
int4,
|
|
16
10
|
int8,
|
|
@@ -20,8 +14,7 @@ import {
|
|
|
20
14
|
textArray,
|
|
21
15
|
timestamptz,
|
|
22
16
|
} from '@prisma-next/target-postgres/contract-free';
|
|
23
|
-
|
|
24
|
-
const CONTROL_CODECS = createAstCodecRegistry(postgresCodecRegistry);
|
|
17
|
+
import { encodeControlQueryParams } from './control-codecs';
|
|
25
18
|
|
|
26
19
|
export const marker = pgTable(
|
|
27
20
|
{ name: 'marker', schema: 'prisma_contract' },
|
|
@@ -105,16 +98,7 @@ export async function execute(
|
|
|
105
98
|
query: AnyQueryAst,
|
|
106
99
|
): Promise<readonly Record<string, unknown>[]> {
|
|
107
100
|
const lowered = lower(query);
|
|
108
|
-
const
|
|
109
|
-
if (slot.kind === 'literal') return slot.value;
|
|
110
|
-
throw new Error('Postgres control DML lowered to a bind parameter, which is unsupported');
|
|
111
|
-
});
|
|
112
|
-
const encoded = await encodeParamsWithMetadata(
|
|
113
|
-
values,
|
|
114
|
-
deriveParamMetadata(query),
|
|
115
|
-
{},
|
|
116
|
-
CONTROL_CODECS,
|
|
117
|
-
);
|
|
101
|
+
const encoded = await encodeControlQueryParams(lowered, query);
|
|
118
102
|
const result = await driver.query(lowered.sql, encoded);
|
|
119
103
|
return result.rows;
|
|
120
104
|
}
|
package/src/core/sql-renderer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonValue } from '@prisma-next/contract/types';
|
|
1
2
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
2
3
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
4
|
import {
|
|
@@ -191,10 +192,11 @@ function renderSelect(ast: SelectAst, contract: PostgresContract, pim: ParamInde
|
|
|
191
192
|
? `GROUP BY ${ast.groupBy.map((expr) => renderExpr(expr, contract, pim)).join(', ')}`
|
|
192
193
|
: '';
|
|
193
194
|
const havingClause = ast.having ? `HAVING ${renderWhere(ast.having, contract, pim)}` : '';
|
|
195
|
+
const sourcesByRef = collectTableSources(ast);
|
|
194
196
|
const orderClause = ast.orderBy?.length
|
|
195
197
|
? `ORDER BY ${ast.orderBy
|
|
196
198
|
.map((order) => {
|
|
197
|
-
const expr =
|
|
199
|
+
const expr = renderOrderByExpr(order.expr, sourcesByRef, contract, pim);
|
|
198
200
|
return `${expr} ${order.dir.toUpperCase()}`;
|
|
199
201
|
})
|
|
200
202
|
.join(', ')}`
|
|
@@ -218,6 +220,125 @@ function renderSelect(ast: SelectAst, contract: PostgresContract, pim: ParamInde
|
|
|
218
220
|
return clauses.trim();
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Storage coordinate a query-level table reference (alias or bare name) resolves to. The ORDER BY enum hook uses this to look a column-ref's storage column up and read its value-set.
|
|
225
|
+
*/
|
|
226
|
+
interface TableSourceCoordinate {
|
|
227
|
+
readonly name: string;
|
|
228
|
+
readonly namespaceId: string | undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Map a SELECT's table references (the FROM source and any JOIN sources) to their storage coordinate, keyed by the name a `ColumnRef.table` would carry (the alias when present, otherwise the table name). Derived-table sources are skipped — their columns are projected through a sub-select, not a base storage column, so the enum hook does not apply.
|
|
233
|
+
*/
|
|
234
|
+
function collectTableSources(ast: SelectAst): ReadonlyMap<string, TableSourceCoordinate> {
|
|
235
|
+
const sources = new Map<string, TableSourceCoordinate>();
|
|
236
|
+
const add = (source: AnyFromSource): void => {
|
|
237
|
+
if (source.kind !== 'table-source') {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const ref = source.alias ?? source.name;
|
|
241
|
+
sources.set(ref, { name: source.name, namespaceId: source.namespaceId });
|
|
242
|
+
};
|
|
243
|
+
add(ast.from);
|
|
244
|
+
for (const join of ast.joins ?? []) {
|
|
245
|
+
add(join.source);
|
|
246
|
+
}
|
|
247
|
+
return sources;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Ordered, codec-encoded values of the value-set a storage column restricts to, or `undefined` when the referenced column carries no value-set (the common, non-enum case). Resolves the column's storage coordinate from the SELECT's table sources, then the column's `valueSet` ref to the value-set's `values`.
|
|
252
|
+
*/
|
|
253
|
+
function allStrings(values: readonly JsonValue[]): values is readonly string[] {
|
|
254
|
+
return values.every((value) => typeof value === 'string');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveEnumOrderValues(
|
|
258
|
+
ref: ColumnRef,
|
|
259
|
+
sourcesByRef: ReadonlyMap<string, TableSourceCoordinate>,
|
|
260
|
+
contract: PostgresContract,
|
|
261
|
+
): readonly JsonValue[] | undefined {
|
|
262
|
+
const source = sourcesByRef.get(ref.table);
|
|
263
|
+
if (source === undefined || source.namespaceId === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
const column =
|
|
267
|
+
contract.storage.namespaces[source.namespaceId]?.entries.table[source.name]?.columns[
|
|
268
|
+
ref.column
|
|
269
|
+
];
|
|
270
|
+
const valueSet = column?.valueSet;
|
|
271
|
+
if (valueSet === undefined) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
return contract.storage.namespaces[valueSet.namespaceId]?.entries.valueSet?.[valueSet.entityName]
|
|
275
|
+
?.values;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Ordered values for an unqualified ORDER BY column (an `identifier-ref`, the shape the sql-builder emits for `.orderBy('col')`). Scans every FROM/JOIN source for a column of that name. Resolves only when exactly one source has a column of that name and it carries a value-set; if more than one source has such a column the bare identifier is ambiguous (regardless of which are enum-backed), so it falls through to the plain column rendering.
|
|
280
|
+
*/
|
|
281
|
+
function resolveEnumOrderValuesForIdentifier(
|
|
282
|
+
name: string,
|
|
283
|
+
sourcesByRef: ReadonlyMap<string, TableSourceCoordinate>,
|
|
284
|
+
contract: PostgresContract,
|
|
285
|
+
): readonly JsonValue[] | undefined {
|
|
286
|
+
let matchedColumns = 0;
|
|
287
|
+
let resolved: readonly JsonValue[] | undefined;
|
|
288
|
+
for (const source of sourcesByRef.values()) {
|
|
289
|
+
if (source.namespaceId === undefined) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const column =
|
|
293
|
+
contract.storage.namespaces[source.namespaceId]?.entries.table[source.name]?.columns[name];
|
|
294
|
+
if (column === undefined) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
matchedColumns += 1;
|
|
298
|
+
if (matchedColumns > 1) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const valueSet = column.valueSet;
|
|
302
|
+
if (valueSet === undefined) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
resolved =
|
|
306
|
+
contract.storage.namespaces[valueSet.namespaceId]?.entries.valueSet?.[valueSet.entityName]
|
|
307
|
+
?.values;
|
|
308
|
+
}
|
|
309
|
+
return resolved;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Render an ORDER BY expression. A column reference onto an enum-restricted column sorts by declaration order via `array_position(ARRAY[…]::text[], <col>)` over the value-set's ordered values (NULLs return `NULL` from `array_position`, sorting per the clause's default NULL handling). Both qualified `column-ref`s and the unqualified `identifier-ref`s the sql-builder emits for `.orderBy('col')` are intercepted. Every other expression renders unchanged.
|
|
314
|
+
*/
|
|
315
|
+
function renderOrderByExpr(
|
|
316
|
+
expr: AnyExpression,
|
|
317
|
+
sourcesByRef: ReadonlyMap<string, TableSourceCoordinate>,
|
|
318
|
+
contract: PostgresContract,
|
|
319
|
+
pim: ParamIndexMap,
|
|
320
|
+
): string {
|
|
321
|
+
// Only TEXT enums lower to a value-set whose ORDER BY rewrite ships in this
|
|
322
|
+
// slice. Numeric-enum ORDER BY is future work; until then a non-string
|
|
323
|
+
// value-set falls through to plain column rendering rather than emitting a
|
|
324
|
+
// wrong numeric-as-text ARRAY.
|
|
325
|
+
if (expr.kind === 'column-ref') {
|
|
326
|
+
const orderValues = resolveEnumOrderValues(expr, sourcesByRef, contract);
|
|
327
|
+
if (orderValues !== undefined && allStrings(orderValues)) {
|
|
328
|
+
const array = orderValues.map((value) => `'${escapeLiteral(value)}'`).join(', ');
|
|
329
|
+
return `array_position(ARRAY[${array}]::text[], ${renderColumn(expr)})`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (expr.kind === 'identifier-ref') {
|
|
333
|
+
const orderValues = resolveEnumOrderValuesForIdentifier(expr.name, sourcesByRef, contract);
|
|
334
|
+
if (orderValues !== undefined && allStrings(orderValues)) {
|
|
335
|
+
const array = orderValues.map((value) => `'${escapeLiteral(value)}'`).join(', ');
|
|
336
|
+
return `array_position(ARRAY[${array}]::text[], ${quoteIdentifier(expr.name)})`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return renderExpr(expr, contract, pim);
|
|
340
|
+
}
|
|
341
|
+
|
|
221
342
|
function renderProjection(
|
|
222
343
|
projection: ReadonlyArray<ProjectionItem>,
|
|
223
344
|
contract: PostgresContract,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-CAlWA4ug.mjs","names":[],"sources":["../src/core/adapter.ts"],"sourcesContent":["import type { CodecLookup } 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 { renderLoweredDdl } from './ddl-renderer';\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: CodecLookup;\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 return renderLoweredDdl(ast);\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":";;;;AAmBA,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,OAAO,iBAAiB,GAAG;EAE7B,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"}
|