@prisma-next/adapter-postgres 0.12.0 → 0.13.0-dev.10
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/README.md +13 -20
- package/dist/{adapter-H8BiuXdq.mjs → adapter-u3NV_jRl.mjs} +13 -13
- package/dist/adapter-u3NV_jRl.mjs.map +1 -0
- package/dist/adapter.d.mts +3 -2
- package/dist/adapter.d.mts.map +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/column-types.d.mts.map +1 -1
- package/dist/control-adapter-Xq5__09s.mjs +1551 -0
- package/dist/control-adapter-Xq5__09s.mjs.map +1 -0
- package/dist/control.d.mts +60 -10
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +4 -660
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-C1wNCHkd.mjs → descriptor-meta-NBwpqHS7.mjs} +1 -1
- package/dist/{descriptor-meta-C1wNCHkd.mjs.map → descriptor-meta-NBwpqHS7.mjs.map} +1 -1
- package/dist/operation-types.d.mts +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +2 -2
- package/dist/{types-B1eiuBHQ.d.mts → types-Dv7M8jx8.d.mts} +1 -1
- package/dist/{types-B1eiuBHQ.d.mts.map → types-Dv7M8jx8.d.mts.map} +1 -1
- package/dist/types.d.mts +2 -2
- package/package.json +24 -24
- package/src/core/adapter.ts +28 -25
- package/src/core/control-adapter.ts +645 -226
- package/src/core/ddl-renderer.ts +155 -0
- package/src/core/enum-control-hooks.ts +2 -2
- package/src/core/marker-ledger.ts +124 -0
- package/src/core/sql-renderer.ts +187 -24
- package/dist/adapter-H8BiuXdq.mjs.map +0 -1
- package/dist/sql-renderer-DlZhVI9B.mjs +0 -457
- package/dist/sql-renderer-DlZhVI9B.mjs.map +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { REFERENTIAL_ACTION_SQL } from '@prisma-next/sql-contract/referential-action-sql';
|
|
2
|
+
import type {
|
|
3
|
+
DdlColumn,
|
|
4
|
+
DdlColumnDefaultVisitor,
|
|
5
|
+
DdlTableConstraint,
|
|
6
|
+
ForeignKeyConstraint,
|
|
7
|
+
FunctionColumnDefault,
|
|
8
|
+
LiteralColumnDefault,
|
|
9
|
+
PrimaryKeyConstraint,
|
|
10
|
+
UniqueConstraint,
|
|
11
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
12
|
+
import type {
|
|
13
|
+
PostgresCreateSchema,
|
|
14
|
+
PostgresCreateTable,
|
|
15
|
+
PostgresDdlNode,
|
|
16
|
+
PostgresDdlVisitor,
|
|
17
|
+
} from '@prisma-next/target-postgres/ddl';
|
|
18
|
+
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
19
|
+
import type { PostgresLoweredStatement } from './types';
|
|
20
|
+
|
|
21
|
+
function quoteQualifiedIdentifier(name: string): string {
|
|
22
|
+
return name.split('.').map(quoteIdentifier).join('.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderPrimaryKeyConstraint(constraint: PrimaryKeyConstraint): string {
|
|
26
|
+
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
27
|
+
if (constraint.name !== undefined) {
|
|
28
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} PRIMARY KEY (${cols})`;
|
|
29
|
+
}
|
|
30
|
+
return `PRIMARY KEY (${cols})`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderForeignKeyConstraint(constraint: ForeignKeyConstraint): string {
|
|
34
|
+
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
35
|
+
const refCols = constraint.refColumns.map(quoteIdentifier).join(', ');
|
|
36
|
+
let sql = `FOREIGN KEY (${cols}) REFERENCES ${quoteQualifiedIdentifier(constraint.refTable)} (${refCols})`;
|
|
37
|
+
if (constraint.onDelete !== undefined) {
|
|
38
|
+
sql += ` ON DELETE ${REFERENTIAL_ACTION_SQL[constraint.onDelete]}`;
|
|
39
|
+
}
|
|
40
|
+
if (constraint.onUpdate !== undefined) {
|
|
41
|
+
sql += ` ON UPDATE ${REFERENTIAL_ACTION_SQL[constraint.onUpdate]}`;
|
|
42
|
+
}
|
|
43
|
+
if (constraint.name !== undefined) {
|
|
44
|
+
sql = `CONSTRAINT ${quoteIdentifier(constraint.name)} ${sql}`;
|
|
45
|
+
}
|
|
46
|
+
return sql;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderUniqueConstraint(constraint: UniqueConstraint): string {
|
|
50
|
+
const cols = constraint.columns.map(quoteIdentifier).join(', ');
|
|
51
|
+
if (constraint.name !== undefined) {
|
|
52
|
+
return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
|
|
53
|
+
}
|
|
54
|
+
return `UNIQUE (${cols})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderTableConstraint(constraint: DdlTableConstraint): string {
|
|
58
|
+
switch (constraint.kind) {
|
|
59
|
+
case 'primary-key':
|
|
60
|
+
return renderPrimaryKeyConstraint(constraint);
|
|
61
|
+
case 'foreign-key':
|
|
62
|
+
return renderForeignKeyConstraint(constraint);
|
|
63
|
+
case 'unique':
|
|
64
|
+
return renderUniqueConstraint(constraint);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class PostgresDdlVisitorImpl implements PostgresDdlVisitor<string> {
|
|
69
|
+
createTable(node: PostgresCreateTable): string {
|
|
70
|
+
const ifNotExists = node.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
71
|
+
const tableRef = node.schema
|
|
72
|
+
? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`
|
|
73
|
+
: quoteIdentifier(node.table);
|
|
74
|
+
const columnDefs = node.columns.map((column) => renderColumn(column));
|
|
75
|
+
const constraintDefs =
|
|
76
|
+
node.constraints !== undefined ? node.constraints.map(renderTableConstraint) : [];
|
|
77
|
+
const allDefs = [...columnDefs, ...constraintDefs].join(',\n ');
|
|
78
|
+
return `CREATE TABLE ${ifNotExists}${tableRef} (\n ${allDefs}\n)`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createSchema(node: PostgresCreateSchema): string {
|
|
82
|
+
const ifNotExists = node.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
83
|
+
return `CREATE SCHEMA ${ifNotExists}${quoteIdentifier(node.schema)}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Postgres infers a quoted-literal default's type as `text` at parse time
|
|
88
|
+
// and applies an implicit text → target coercion at default-evaluation
|
|
89
|
+
// time. That coercion exists for some target types (text, jsonb, json)
|
|
90
|
+
// and not others (uuid, inet, timestamptz, citext, enums, user-defined
|
|
91
|
+
// types, geometry/geography from PostGIS, array types). Quoted-literal
|
|
92
|
+
// defaults on any non-text column therefore need an explicit
|
|
93
|
+
// `::<nativeType>` cast in the emitted DDL — both to state the default's
|
|
94
|
+
// intent (the literal IS that type, not text that happens to coerce) and
|
|
95
|
+
// to keep emitted migrations correct for types where no implicit cast
|
|
96
|
+
// exists. Numeric, boolean, and null literals are typed by Postgres
|
|
97
|
+
// directly (no `text` indirection) so they need no cast.
|
|
98
|
+
function isTextLikeNativeType(nativeType: string): boolean {
|
|
99
|
+
return (
|
|
100
|
+
nativeType === 'text' ||
|
|
101
|
+
nativeType === 'varchar' ||
|
|
102
|
+
nativeType.startsWith('varchar(') ||
|
|
103
|
+
nativeType === 'character varying' ||
|
|
104
|
+
nativeType.startsWith('character varying(') ||
|
|
105
|
+
nativeType === 'char' ||
|
|
106
|
+
nativeType.startsWith('char(') ||
|
|
107
|
+
nativeType === 'character' ||
|
|
108
|
+
nativeType.startsWith('character(')
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const defaultVisitor: DdlColumnDefaultVisitor<string> = {
|
|
113
|
+
literal(node: LiteralColumnDefault, ctx): string {
|
|
114
|
+
const { value } = node;
|
|
115
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
116
|
+
return `DEFAULT ${String(value)}`;
|
|
117
|
+
}
|
|
118
|
+
if (value === null) {
|
|
119
|
+
return 'DEFAULT NULL';
|
|
120
|
+
}
|
|
121
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
122
|
+
const literal = `'${escapeLiteral(serialized)}'`;
|
|
123
|
+
return isTextLikeNativeType(ctx.nativeType)
|
|
124
|
+
? `DEFAULT ${literal}`
|
|
125
|
+
: `DEFAULT ${literal}::${ctx.nativeType}`;
|
|
126
|
+
},
|
|
127
|
+
function(node: FunctionColumnDefault, _ctx): string {
|
|
128
|
+
if (node.expression === 'autoincrement()') {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
return `DEFAULT (${node.expression})`;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function renderColumn(column: DdlColumn): string {
|
|
136
|
+
const parts = [quoteIdentifier(column.name), column.type];
|
|
137
|
+
if (column.notNull) {
|
|
138
|
+
parts.push('NOT NULL');
|
|
139
|
+
}
|
|
140
|
+
if (column.primaryKey) {
|
|
141
|
+
parts.push('PRIMARY KEY');
|
|
142
|
+
}
|
|
143
|
+
const defaultClause = column.default
|
|
144
|
+
? column.default.accept(defaultVisitor, { nativeType: column.type })
|
|
145
|
+
: '';
|
|
146
|
+
if (defaultClause.length > 0) {
|
|
147
|
+
parts.push(defaultClause);
|
|
148
|
+
}
|
|
149
|
+
return parts.join(' ');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderLoweredDdl(ast: PostgresDdlNode): PostgresLoweredStatement {
|
|
153
|
+
const sql = ast.accept(new PostgresDdlVisitorImpl());
|
|
154
|
+
return Object.freeze({ sql, params: Object.freeze([]) });
|
|
155
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
|
|
2
2
|
import { PG_ENUM_CODEC_ID } from '@prisma-next/target-postgres/codec-ids';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -117,7 +117,7 @@ function parseArrayElements(input: string): string[] {
|
|
|
117
117
|
* (which writes them under `schema.annotations.pg.storageTypes`).
|
|
118
118
|
*/
|
|
119
119
|
export async function introspectPostgresEnumTypes(options: {
|
|
120
|
-
readonly driver:
|
|
120
|
+
readonly driver: SqlControlDriverInstance<'postgres'>;
|
|
121
121
|
readonly schemaName?: string;
|
|
122
122
|
}): Promise<Record<string, PostgresEnumStorageTypeAnnotation>> {
|
|
123
123
|
const namespace = options.schemaName ?? 'public';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
|
|
2
|
+
import {
|
|
3
|
+
type AnyQueryAst,
|
|
4
|
+
type LoweredStatement,
|
|
5
|
+
RawExpr,
|
|
6
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
7
|
+
import {
|
|
8
|
+
createAstCodecRegistry,
|
|
9
|
+
deriveParamMetadata,
|
|
10
|
+
encodeParamsWithMetadata,
|
|
11
|
+
} from '@prisma-next/sql-runtime';
|
|
12
|
+
import { PG_TIMESTAMPTZ_CODEC_ID } from '@prisma-next/target-postgres/codec-ids';
|
|
13
|
+
import { postgresCodecRegistry } from '@prisma-next/target-postgres/codecs';
|
|
14
|
+
import {
|
|
15
|
+
int4,
|
|
16
|
+
int8,
|
|
17
|
+
jsonb,
|
|
18
|
+
pgTable,
|
|
19
|
+
text,
|
|
20
|
+
textArray,
|
|
21
|
+
timestamptz,
|
|
22
|
+
} from '@prisma-next/target-postgres/contract-free';
|
|
23
|
+
|
|
24
|
+
const CONTROL_CODECS = createAstCodecRegistry(postgresCodecRegistry);
|
|
25
|
+
|
|
26
|
+
export const marker = pgTable(
|
|
27
|
+
{ name: 'marker', schema: 'prisma_contract' },
|
|
28
|
+
{
|
|
29
|
+
space: text(),
|
|
30
|
+
core_hash: text(),
|
|
31
|
+
profile_hash: text(),
|
|
32
|
+
contract_json: jsonb({ nullable: true }),
|
|
33
|
+
canonical_version: int4({ nullable: true }),
|
|
34
|
+
updated_at: timestamptz(),
|
|
35
|
+
app_tag: text({ nullable: true }),
|
|
36
|
+
meta: jsonb({ nullable: true }),
|
|
37
|
+
invariants: textArray(),
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Writeable subset of the `prisma_contract.ledger` table. Omits the
|
|
43
|
+
* DB-generated `id` (bigserial) and `created_at` (default `now()`) so the
|
|
44
|
+
* insert path doesn't have to pass them.
|
|
45
|
+
*/
|
|
46
|
+
export const ledger = pgTable(
|
|
47
|
+
{ name: 'ledger', schema: 'prisma_contract' },
|
|
48
|
+
{
|
|
49
|
+
space: text(),
|
|
50
|
+
migration_name: text(),
|
|
51
|
+
migration_hash: text(),
|
|
52
|
+
origin_core_hash: text({ nullable: true }),
|
|
53
|
+
destination_core_hash: text(),
|
|
54
|
+
operations: jsonb(),
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read-side handle covering every column of `prisma_contract.ledger`,
|
|
60
|
+
* including the DB-generated `id` (for ORDER BY) and `created_at`.
|
|
61
|
+
*/
|
|
62
|
+
export const ledgerReadShape = pgTable(
|
|
63
|
+
{ name: 'ledger', schema: 'prisma_contract' },
|
|
64
|
+
{
|
|
65
|
+
id: int8(),
|
|
66
|
+
space: text(),
|
|
67
|
+
migration_name: text(),
|
|
68
|
+
migration_hash: text(),
|
|
69
|
+
origin_core_hash: text({ nullable: true }),
|
|
70
|
+
destination_core_hash: text(),
|
|
71
|
+
operations: jsonb(),
|
|
72
|
+
created_at: timestamptz(),
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export const infoSchemaTables = pgTable(
|
|
77
|
+
{ name: 'tables', schema: 'information_schema' },
|
|
78
|
+
{ table_schema: text(), table_name: text() },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const NOW = new RawExpr({
|
|
82
|
+
parts: ['now()'],
|
|
83
|
+
returns: { codecId: PG_TIMESTAMPTZ_CODEC_ID, nullable: false },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
type Lower = (query: AnyQueryAst) => LoweredStatement;
|
|
87
|
+
|
|
88
|
+
type MarkerDriver = {
|
|
89
|
+
query<Row = Record<string, unknown>>(
|
|
90
|
+
sql: string,
|
|
91
|
+
params?: readonly unknown[],
|
|
92
|
+
): Promise<{ readonly rows: ReadonlyArray<Row> }>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function mergeInvariants(
|
|
96
|
+
current: readonly string[],
|
|
97
|
+
incoming: readonly string[],
|
|
98
|
+
): readonly string[] {
|
|
99
|
+
return [...new Set([...current, ...incoming])].sort();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function execute(
|
|
103
|
+
lower: Lower,
|
|
104
|
+
driver: MarkerDriver,
|
|
105
|
+
query: AnyQueryAst,
|
|
106
|
+
): Promise<readonly Record<string, unknown>[]> {
|
|
107
|
+
const lowered = lower(query);
|
|
108
|
+
const values = lowered.params.map((slot) => {
|
|
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
|
+
);
|
|
118
|
+
const result = await driver.query(lowered.sql, encoded);
|
|
119
|
+
return result.rows;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type PostgresMarkerDriver = MarkerDriver;
|
|
123
|
+
export type PostgresMarkerLower = Lower;
|
|
124
|
+
export type PostgresMarkerWriteDriver = SqlControlDriverInstance<'postgres'>;
|
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 {
|
|
@@ -27,9 +28,11 @@ import {
|
|
|
27
28
|
type RawSqlExpr,
|
|
28
29
|
type SelectAst,
|
|
29
30
|
type SubqueryExpr,
|
|
31
|
+
type TableSource,
|
|
30
32
|
type UpdateAst,
|
|
31
33
|
type WindowFuncExpr,
|
|
32
34
|
} from '@prisma-next/sql-relational-core/ast';
|
|
35
|
+
import { PostgresTableSource } from '@prisma-next/target-postgres/contract-free';
|
|
33
36
|
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
34
37
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
35
38
|
import type { PostgresContract } from './types';
|
|
@@ -189,10 +192,11 @@ function renderSelect(ast: SelectAst, contract: PostgresContract, pim: ParamInde
|
|
|
189
192
|
? `GROUP BY ${ast.groupBy.map((expr) => renderExpr(expr, contract, pim)).join(', ')}`
|
|
190
193
|
: '';
|
|
191
194
|
const havingClause = ast.having ? `HAVING ${renderWhere(ast.having, contract, pim)}` : '';
|
|
195
|
+
const sourcesByRef = collectTableSources(ast);
|
|
192
196
|
const orderClause = ast.orderBy?.length
|
|
193
197
|
? `ORDER BY ${ast.orderBy
|
|
194
198
|
.map((order) => {
|
|
195
|
-
const expr =
|
|
199
|
+
const expr = renderOrderByExpr(order.expr, sourcesByRef, contract, pim);
|
|
196
200
|
return `${expr} ${order.dir.toUpperCase()}`;
|
|
197
201
|
})
|
|
198
202
|
.join(', ')}`
|
|
@@ -216,6 +220,124 @@ function renderSelect(ast: SelectAst, contract: PostgresContract, pim: ParamInde
|
|
|
216
220
|
return clauses.trim();
|
|
217
221
|
}
|
|
218
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.name]
|
|
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.name]?.values;
|
|
307
|
+
}
|
|
308
|
+
return resolved;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 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.
|
|
313
|
+
*/
|
|
314
|
+
function renderOrderByExpr(
|
|
315
|
+
expr: AnyExpression,
|
|
316
|
+
sourcesByRef: ReadonlyMap<string, TableSourceCoordinate>,
|
|
317
|
+
contract: PostgresContract,
|
|
318
|
+
pim: ParamIndexMap,
|
|
319
|
+
): string {
|
|
320
|
+
// Only TEXT enums lower to a value-set whose ORDER BY rewrite ships in this
|
|
321
|
+
// slice. Numeric-enum ORDER BY is future work; until then a non-string
|
|
322
|
+
// value-set falls through to plain column rendering rather than emitting a
|
|
323
|
+
// wrong numeric-as-text ARRAY.
|
|
324
|
+
if (expr.kind === 'column-ref') {
|
|
325
|
+
const orderValues = resolveEnumOrderValues(expr, sourcesByRef, contract);
|
|
326
|
+
if (orderValues !== undefined && allStrings(orderValues)) {
|
|
327
|
+
const array = orderValues.map((value) => `'${escapeLiteral(value)}'`).join(', ');
|
|
328
|
+
return `array_position(ARRAY[${array}]::text[], ${renderColumn(expr)})`;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (expr.kind === 'identifier-ref') {
|
|
332
|
+
const orderValues = resolveEnumOrderValuesForIdentifier(expr.name, sourcesByRef, contract);
|
|
333
|
+
if (orderValues !== undefined && allStrings(orderValues)) {
|
|
334
|
+
const array = orderValues.map((value) => `'${escapeLiteral(value)}'`).join(', ');
|
|
335
|
+
return `array_position(ARRAY[${array}]::text[], ${quoteIdentifier(expr.name)})`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return renderExpr(expr, contract, pim);
|
|
339
|
+
}
|
|
340
|
+
|
|
219
341
|
function renderProjection(
|
|
220
342
|
projection: ReadonlyArray<ProjectionItem>,
|
|
221
343
|
contract: PostgresContract,
|
|
@@ -269,6 +391,39 @@ function renderDistinctPrefix(
|
|
|
269
391
|
return '';
|
|
270
392
|
}
|
|
271
393
|
|
|
394
|
+
function qualifyTableFromNamespaceCoordinate(
|
|
395
|
+
table: Pick<TableSource, 'name' | 'namespaceId'>,
|
|
396
|
+
contract: PostgresContract,
|
|
397
|
+
): string {
|
|
398
|
+
if (table instanceof PostgresTableSource && table.schema !== undefined) {
|
|
399
|
+
return `${quoteIdentifier(table.schema)}.${quoteIdentifier(table.name)}`;
|
|
400
|
+
}
|
|
401
|
+
if (table.namespaceId === undefined) {
|
|
402
|
+
return quoteIdentifier(table.name);
|
|
403
|
+
}
|
|
404
|
+
const namespace = contract.storage.namespaces[table.namespaceId];
|
|
405
|
+
if (namespace === undefined) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Table "${table.name}" references namespace "${table.namespaceId}" which is not present as a Postgres schema on the contract`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const qualifyTable = namespace.qualifyTable;
|
|
411
|
+
if (qualifyTable === undefined) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Table "${table.name}" references namespace "${table.namespaceId}" which is not materialised as a Postgres schema on the contract`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return qualifyTable.call(namespace, table.name);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function renderTableSource(source: TableSource, contract: PostgresContract): string {
|
|
420
|
+
const qualified = qualifyTableFromNamespaceCoordinate(source, contract);
|
|
421
|
+
if (!source.alias) {
|
|
422
|
+
return qualified;
|
|
423
|
+
}
|
|
424
|
+
return `${qualified} AS ${quoteIdentifier(source.alias)}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
272
427
|
function renderSource(
|
|
273
428
|
source: AnyFromSource,
|
|
274
429
|
contract: PostgresContract,
|
|
@@ -276,13 +431,8 @@ function renderSource(
|
|
|
276
431
|
): string {
|
|
277
432
|
const node = source;
|
|
278
433
|
switch (node.kind) {
|
|
279
|
-
case 'table-source':
|
|
280
|
-
|
|
281
|
-
if (!node.alias) {
|
|
282
|
-
return table;
|
|
283
|
-
}
|
|
284
|
-
return `${table} AS ${quoteIdentifier(node.alias)}`;
|
|
285
|
-
}
|
|
434
|
+
case 'table-source':
|
|
435
|
+
return renderTableSource(node, contract);
|
|
286
436
|
case 'derived-table-source':
|
|
287
437
|
return `(${renderSelect(node.query, contract, pim)}) AS ${quoteIdentifier(node.alias)}`;
|
|
288
438
|
// v8 ignore next 4
|
|
@@ -659,8 +809,9 @@ function renderJoinOn(on: JoinOnExpr, contract: PostgresContract, pim: ParamInde
|
|
|
659
809
|
function getInsertColumnOrder(
|
|
660
810
|
rows: ReadonlyArray<Record<string, InsertValue>>,
|
|
661
811
|
contract: PostgresContract,
|
|
662
|
-
|
|
812
|
+
tableRef: Pick<TableSource, 'name' | 'namespaceId'>,
|
|
663
813
|
): string[] {
|
|
814
|
+
const tableName = tableRef.name;
|
|
664
815
|
const orderedColumns: string[] = [];
|
|
665
816
|
const seenColumns = new Set<string>();
|
|
666
817
|
|
|
@@ -678,14 +829,18 @@ function getInsertColumnOrder(
|
|
|
678
829
|
return orderedColumns;
|
|
679
830
|
}
|
|
680
831
|
|
|
681
|
-
let table: { columns: Record<string, unknown
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
832
|
+
let table: { columns: Readonly<Record<string, unknown>> } | undefined;
|
|
833
|
+
if (tableRef.namespaceId !== undefined) {
|
|
834
|
+
const ns = contract.storage.namespaces[tableRef.namespaceId];
|
|
835
|
+
table = ns?.entries.table[tableName];
|
|
836
|
+
}
|
|
837
|
+
if (table === undefined) {
|
|
838
|
+
for (const ns of Object.values(contract.storage.namespaces)) {
|
|
839
|
+
const found = ns.entries.table[tableName];
|
|
840
|
+
if (found !== undefined) {
|
|
841
|
+
table = found;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
689
844
|
}
|
|
690
845
|
}
|
|
691
846
|
if (!table) {
|
|
@@ -694,7 +849,11 @@ function getInsertColumnOrder(
|
|
|
694
849
|
return Object.keys(table.columns);
|
|
695
850
|
}
|
|
696
851
|
|
|
697
|
-
function renderInsertValue(
|
|
852
|
+
function renderInsertValue(
|
|
853
|
+
value: InsertValue | undefined,
|
|
854
|
+
contract: PostgresContract,
|
|
855
|
+
pim: ParamIndexMap,
|
|
856
|
+
): string {
|
|
698
857
|
if (!value || value.kind === 'default-value') {
|
|
699
858
|
return 'DEFAULT';
|
|
700
859
|
}
|
|
@@ -705,6 +864,8 @@ function renderInsertValue(value: InsertValue | undefined, pim: ParamIndexMap):
|
|
|
705
864
|
return renderParamRef(value, pim);
|
|
706
865
|
case 'column-ref':
|
|
707
866
|
return renderColumn(value);
|
|
867
|
+
case 'raw-expr':
|
|
868
|
+
return renderExpr(value, contract, pim);
|
|
708
869
|
// v8 ignore next 4
|
|
709
870
|
default:
|
|
710
871
|
throw new Error(
|
|
@@ -714,7 +875,7 @@ function renderInsertValue(value: InsertValue | undefined, pim: ParamIndexMap):
|
|
|
714
875
|
}
|
|
715
876
|
|
|
716
877
|
function renderInsert(ast: InsertAst, contract: PostgresContract, pim: ParamIndexMap): string {
|
|
717
|
-
const table =
|
|
878
|
+
const table = qualifyTableFromNamespaceCoordinate(ast.table, contract);
|
|
718
879
|
const rows = ast.rows;
|
|
719
880
|
if (rows.length === 0) {
|
|
720
881
|
throw new Error('INSERT requires at least one row');
|
|
@@ -726,7 +887,7 @@ function renderInsert(ast: InsertAst, contract: PostgresContract, pim: ParamInde
|
|
|
726
887
|
return `INSERT INTO ${table} DEFAULT VALUES`;
|
|
727
888
|
}
|
|
728
889
|
|
|
729
|
-
const defaultColumns = getInsertColumnOrder(rows, contract, ast.table
|
|
890
|
+
const defaultColumns = getInsertColumnOrder(rows, contract, ast.table);
|
|
730
891
|
if (defaultColumns.length === 0) {
|
|
731
892
|
return `INSERT INTO ${table} VALUES ${rows.map(() => '()').join(', ')}`;
|
|
732
893
|
}
|
|
@@ -738,11 +899,13 @@ function renderInsert(ast: InsertAst, contract: PostgresContract, pim: ParamInde
|
|
|
738
899
|
.join(', ')}`;
|
|
739
900
|
}
|
|
740
901
|
|
|
741
|
-
const columnOrder = getInsertColumnOrder(rows, contract, ast.table
|
|
902
|
+
const columnOrder = getInsertColumnOrder(rows, contract, ast.table);
|
|
742
903
|
const columns = columnOrder.map((column) => quoteIdentifier(column));
|
|
743
904
|
const values = rows
|
|
744
905
|
.map((row) => {
|
|
745
|
-
const renderedRow = columnOrder.map((column) =>
|
|
906
|
+
const renderedRow = columnOrder.map((column) =>
|
|
907
|
+
renderInsertValue(row[column], contract, pim),
|
|
908
|
+
);
|
|
746
909
|
return `(${renderedRow.join(', ')})`;
|
|
747
910
|
})
|
|
748
911
|
.join(', ');
|
|
@@ -786,7 +949,7 @@ function renderInsert(ast: InsertAst, contract: PostgresContract, pim: ParamInde
|
|
|
786
949
|
}
|
|
787
950
|
|
|
788
951
|
function renderUpdate(ast: UpdateAst, contract: PostgresContract, pim: ParamIndexMap): string {
|
|
789
|
-
const table =
|
|
952
|
+
const table = qualifyTableFromNamespaceCoordinate(ast.table, contract);
|
|
790
953
|
const setEntries = Object.entries(ast.set);
|
|
791
954
|
if (setEntries.length === 0) {
|
|
792
955
|
throw new Error('UPDATE requires at least one SET assignment');
|
|
@@ -805,7 +968,7 @@ function renderUpdate(ast: UpdateAst, contract: PostgresContract, pim: ParamInde
|
|
|
805
968
|
}
|
|
806
969
|
|
|
807
970
|
function renderDelete(ast: DeleteAst, contract: PostgresContract, pim: ParamIndexMap): string {
|
|
808
|
-
const table =
|
|
971
|
+
const table = qualifyTableFromNamespaceCoordinate(ast.table, contract);
|
|
809
972
|
const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : '';
|
|
810
973
|
const returningClause = ast.returning?.length
|
|
811
974
|
? ` RETURNING ${renderReturning(ast.returning, contract, pim)}`
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-H8BiuXdq.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 MarkerReadResult,\n RawSqlLiteral,\n SqlQueryable,\n} from '@prisma-next/sql-relational-core/ast';\nimport type { RawCodecInferer } from '@prisma-next/sql-relational-core/expression';\nimport { parseContractMarkerRow } from '@prisma-next/sql-runtime';\nimport { createPostgresBuiltinCodecLookup } from './codec-lookup';\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 this.profile = Object.freeze({\n id: options?.profileId ?? 'postgres/default@1',\n target: 'postgres',\n capabilities: defaultCapabilities,\n readMarker: (queryable: SqlQueryable) => readPostgresMarker(queryable),\n });\n }\n\n lower(ast: AnyQueryAst, context: LowererContext<PostgresContract>): PostgresLoweredStatement {\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\nasync function readPostgresMarker(queryable: SqlQueryable): Promise<MarkerReadResult> {\n const exists = await queryable.query(\n 'select 1 from information_schema.tables where table_schema = $1 and table_name = $2',\n ['prisma_contract', 'marker'],\n );\n if (exists.rows.length === 0) {\n return { kind: 'no-table' };\n }\n\n const result = await queryable.query(\n 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from prisma_contract.marker where space = $1',\n [APP_SPACE_ID],\n );\n const row = result.rows[0];\n if (!row) {\n return { kind: 'absent' };\n }\n // Postgres' driver hydrates `text[]` columns as native JS arrays, so the row is already in the shape the shared parser expects.\n return { kind: 'present', record: parseContractMarkerRow(row) };\n}\n\nexport function createPostgresAdapter(options?: PostgresAdapterOptions) {\n return Object.freeze(new PostgresAdapterImpl(options));\n}\n"],"mappings":";;;;AAiBA,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,KAAK,UAAU,OAAO,OAAO;GAC3B,IAAI,SAAS,aAAa;GAC1B,QAAQ;GACR,cAAc;GACd,aAAa,cAA4B,mBAAmB,SAAS;EACvE,CAAC;CACH;CAEA,MAAM,KAAkB,SAAqE;EAC3F,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,eAAe,mBAAmB,WAAoD;CAKpF,KAAI,MAJiB,UAAU,MAC7B,uFACA,CAAC,mBAAmB,QAAQ,CAC9B,GACW,KAAK,WAAW,GACzB,OAAO,EAAE,MAAM,WAAW;CAO5B,MAAM,OAAM,MAJS,UAAU,MAC7B,wJACA,CAAC,YAAY,CACf,GACmB,KAAK;CACxB,IAAI,CAAC,KACH,OAAO,EAAE,MAAM,SAAS;CAG1B,OAAO;EAAE,MAAM;EAAW,QAAQ,uBAAuB,GAAG;CAAE;AAChE;AAEA,SAAgB,sBAAsB,SAAkC;CACtE,OAAO,OAAO,OAAO,IAAI,oBAAoB,OAAO,CAAC;AACvD"}
|