@prisma-next/adapter-postgres 0.12.0-dev.6 → 0.12.0-dev.61
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-H8BiuXdq.mjs → adapter-DctzSWYj.mjs} +13 -13
- package/dist/adapter-DctzSWYj.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-B7tCv02A.mjs +1357 -0
- package/dist/control-adapter-B7tCv02A.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 +380 -105
- package/src/core/ddl-renderer.ts +73 -0
- package/src/core/enum-control-hooks.ts +2 -2
- package/src/core/marker-ledger.ts +124 -0
- package/src/core/sql-renderer.ts +66 -23
- 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
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ContractMarkerRecord,
|
|
4
|
+
LedgerEntryRecord,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
6
|
+
import {
|
|
7
|
+
parseMarkerRowSafely,
|
|
8
|
+
rethrowMarkerReadError,
|
|
9
|
+
withMarkerReadErrorHandling,
|
|
10
|
+
} from '@prisma-next/errors/execution';
|
|
3
11
|
import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
|
|
4
12
|
import { parseContractMarkerRow } from '@prisma-next/family-sql/verify';
|
|
5
13
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
6
|
-
import {
|
|
7
|
-
APP_SPACE_ID,
|
|
8
|
-
type ControlDriverInstance,
|
|
9
|
-
} from '@prisma-next/framework-components/control';
|
|
14
|
+
import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
|
|
10
15
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
11
|
-
import
|
|
16
|
+
import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
|
|
17
|
+
import type {
|
|
18
|
+
PostgresEnumStorageEntry,
|
|
19
|
+
SqlControlDriverInstance,
|
|
20
|
+
SqlStorage,
|
|
21
|
+
} from '@prisma-next/sql-contract/types';
|
|
12
22
|
import type {
|
|
13
23
|
AnyQueryAst,
|
|
24
|
+
DdlNode,
|
|
14
25
|
LoweredStatement,
|
|
15
26
|
LowererContext,
|
|
27
|
+
MarkerReadResult,
|
|
16
28
|
} from '@prisma-next/sql-relational-core/ast';
|
|
29
|
+
import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
|
|
17
30
|
import type {
|
|
18
31
|
PrimaryKey,
|
|
19
32
|
SqlColumnIR,
|
|
@@ -24,6 +37,11 @@ import type {
|
|
|
24
37
|
SqlTableIR,
|
|
25
38
|
SqlUniqueIR,
|
|
26
39
|
} from '@prisma-next/sql-schema-ir/types';
|
|
40
|
+
import {
|
|
41
|
+
buildControlTableBootstrapQueries,
|
|
42
|
+
buildSignMarkerBootstrapQueries,
|
|
43
|
+
} from '@prisma-next/target-postgres/contract-free';
|
|
44
|
+
import type { PostgresDdlNode } from '@prisma-next/target-postgres/ddl';
|
|
27
45
|
import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
|
|
28
46
|
import {
|
|
29
47
|
createResolveExistingEnumValues,
|
|
@@ -35,14 +53,35 @@ import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-t
|
|
|
35
53
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
36
54
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
37
55
|
import { createPostgresBuiltinCodecLookup } from './codec-lookup';
|
|
56
|
+
import { renderLoweredDdl } from './ddl-renderer';
|
|
38
57
|
import {
|
|
39
58
|
introspectPostgresEnumTypes,
|
|
40
59
|
type PostgresEnumStorageTypeAnnotation,
|
|
41
60
|
} from './enum-control-hooks';
|
|
61
|
+
import {
|
|
62
|
+
execute,
|
|
63
|
+
infoSchemaTables,
|
|
64
|
+
ledger,
|
|
65
|
+
ledgerReadShape,
|
|
66
|
+
marker,
|
|
67
|
+
mergeInvariants,
|
|
68
|
+
NOW,
|
|
69
|
+
} from './marker-ledger';
|
|
42
70
|
import { renderLoweredSql } from './sql-renderer';
|
|
43
71
|
import type { PostgresContract } from './types';
|
|
44
72
|
|
|
45
73
|
const POSTGRES_MARKER_TABLE = 'prisma_contract.marker';
|
|
74
|
+
const POSTGRES_LEDGER_TABLE = 'prisma_contract.ledger';
|
|
75
|
+
|
|
76
|
+
type PostgresLedgerRow = {
|
|
77
|
+
readonly space: string;
|
|
78
|
+
readonly migration_name: string;
|
|
79
|
+
readonly migration_hash: string;
|
|
80
|
+
readonly origin_core_hash: string | null;
|
|
81
|
+
readonly destination_core_hash: string;
|
|
82
|
+
readonly operations: unknown;
|
|
83
|
+
readonly created_at: Date | string;
|
|
84
|
+
};
|
|
46
85
|
|
|
47
86
|
/**
|
|
48
87
|
* Postgres control plane adapter for control-plane operations like introspection.
|
|
@@ -99,6 +138,14 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
99
138
|
readonly resolveExistingEnumValuesForContract = (contract: Contract<SqlStorage>) =>
|
|
100
139
|
createResolveExistingEnumValues(contract.storage);
|
|
101
140
|
|
|
141
|
+
bootstrapControlTableQueries(): readonly DdlNode[] {
|
|
142
|
+
return buildControlTableBootstrapQueries();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
bootstrapSignMarkerQueries(): readonly DdlNode[] {
|
|
146
|
+
return buildSignMarkerBootstrapQueries();
|
|
147
|
+
}
|
|
148
|
+
|
|
102
149
|
/**
|
|
103
150
|
* Lower a SQL query AST into a Postgres-flavored `{ sql, params }` payload.
|
|
104
151
|
*
|
|
@@ -107,7 +154,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
107
154
|
* and contract. Used at migration plan/emit time (e.g. by `dataTransform`)
|
|
108
155
|
* without instantiating the runtime adapter.
|
|
109
156
|
*/
|
|
110
|
-
lower(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
|
|
157
|
+
lower(ast: AnyQueryAst | PostgresDdlNode, context: LowererContext<unknown>): LoweredStatement {
|
|
158
|
+
if (isDdlNode(ast)) {
|
|
159
|
+
return renderLoweredDdl(ast);
|
|
160
|
+
}
|
|
111
161
|
return renderLoweredSql(ast, context.contract as PostgresContract, this.codecLookup);
|
|
112
162
|
}
|
|
113
163
|
|
|
@@ -120,55 +170,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
120
170
|
* parse errors, so we probe before reading.
|
|
121
171
|
*/
|
|
122
172
|
async readMarker(
|
|
123
|
-
driver:
|
|
173
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
124
174
|
space: string,
|
|
125
175
|
): Promise<ContractMarkerRecord | null> {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
driver.query(
|
|
130
|
-
`select 1
|
|
131
|
-
from information_schema.tables
|
|
132
|
-
where table_schema = $1 and table_name = $2`,
|
|
133
|
-
['prisma_contract', 'marker'],
|
|
134
|
-
),
|
|
135
|
-
markerContext,
|
|
136
|
-
);
|
|
137
|
-
if (exists.rows.length === 0) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const result = await withMarkerReadErrorHandling(
|
|
142
|
-
() =>
|
|
143
|
-
driver.query<{
|
|
144
|
-
core_hash: string;
|
|
145
|
-
profile_hash: string;
|
|
146
|
-
contract_json: unknown | null;
|
|
147
|
-
canonical_version: number | null;
|
|
148
|
-
updated_at: Date | string;
|
|
149
|
-
app_tag: string | null;
|
|
150
|
-
meta: unknown | null;
|
|
151
|
-
invariants: readonly string[];
|
|
152
|
-
}>(
|
|
153
|
-
`select
|
|
154
|
-
core_hash,
|
|
155
|
-
profile_hash,
|
|
156
|
-
contract_json,
|
|
157
|
-
canonical_version,
|
|
158
|
-
updated_at,
|
|
159
|
-
app_tag,
|
|
160
|
-
meta,
|
|
161
|
-
invariants
|
|
162
|
-
from prisma_contract.marker
|
|
163
|
-
where space = $1`,
|
|
164
|
-
[space],
|
|
165
|
-
),
|
|
166
|
-
markerContext,
|
|
167
|
-
);
|
|
176
|
+
const result = await this.readMarkerDiscriminated(driver, space);
|
|
177
|
+
return result.kind === 'present' ? result.record : null;
|
|
178
|
+
}
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
async readMarkerDiscriminated(
|
|
181
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
182
|
+
space: string,
|
|
183
|
+
): Promise<MarkerReadResult> {
|
|
184
|
+
const markerContext = { space, markerLocation: POSTGRES_MARKER_TABLE };
|
|
185
|
+
return withMarkerReadErrorHandling(() => this.readMarkerResult(driver, space), markerContext);
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
/**
|
|
@@ -178,54 +192,53 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
178
192
|
* map rather than raising "relation does not exist".
|
|
179
193
|
*/
|
|
180
194
|
async readAllMarkers(
|
|
181
|
-
driver:
|
|
195
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
182
196
|
): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
183
197
|
const markerContext = { space: APP_SPACE_ID, markerLocation: POSTGRES_MARKER_TABLE };
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
return withMarkerReadErrorHandling(() => this.readAllMarkersResult(driver), markerContext);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async readAllMarkersResult(
|
|
202
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
203
|
+
): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
204
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
205
|
+
const probe = infoSchemaTables
|
|
206
|
+
.select(infoSchemaTables.table_schema)
|
|
207
|
+
.where(
|
|
208
|
+
infoSchemaTables.table_schema
|
|
209
|
+
.eq('prisma_contract')
|
|
210
|
+
.and(infoSchemaTables.table_name.eq('marker')),
|
|
211
|
+
)
|
|
212
|
+
.build();
|
|
213
|
+
const exists = await execute(lower, driver, probe);
|
|
214
|
+
if (exists.length === 0) {
|
|
195
215
|
return new Map();
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
|
|
199
|
-
() =>
|
|
200
|
-
driver.query<{
|
|
201
|
-
space: string;
|
|
202
|
-
core_hash: string;
|
|
203
|
-
profile_hash: string;
|
|
204
|
-
contract_json: unknown | null;
|
|
205
|
-
canonical_version: number | null;
|
|
206
|
-
updated_at: Date | string;
|
|
207
|
-
app_tag: string | null;
|
|
208
|
-
meta: unknown | null;
|
|
209
|
-
invariants: readonly string[];
|
|
210
|
-
}>(
|
|
211
|
-
`select
|
|
212
|
-
space,
|
|
213
|
-
core_hash,
|
|
214
|
-
profile_hash,
|
|
215
|
-
contract_json,
|
|
216
|
-
canonical_version,
|
|
217
|
-
updated_at,
|
|
218
|
-
app_tag,
|
|
219
|
-
meta,
|
|
220
|
-
invariants
|
|
221
|
-
from prisma_contract.marker`,
|
|
222
|
-
),
|
|
223
|
-
markerContext,
|
|
224
|
-
);
|
|
218
|
+
await this.assertMarkerTableHasSpaceColumn(driver, APP_SPACE_ID);
|
|
225
219
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
const fetch = marker
|
|
221
|
+
.select(
|
|
222
|
+
marker.space,
|
|
223
|
+
marker.core_hash,
|
|
224
|
+
marker.profile_hash,
|
|
225
|
+
marker.contract_json,
|
|
226
|
+
marker.canonical_version,
|
|
227
|
+
marker.updated_at,
|
|
228
|
+
marker.app_tag,
|
|
229
|
+
marker.meta,
|
|
230
|
+
marker.invariants,
|
|
231
|
+
)
|
|
232
|
+
.build();
|
|
233
|
+
const rawRows = await execute(lower, driver, fetch);
|
|
234
|
+
const rows = blindCast<
|
|
235
|
+
ReadonlyArray<{ space: string } & Record<string, unknown>>,
|
|
236
|
+
'Driver returns rows shaped by SELECT'
|
|
237
|
+
>(rawRows);
|
|
238
|
+
|
|
239
|
+
const out = new Map<string, ContractMarkerRecord>();
|
|
240
|
+
for (const row of rows) {
|
|
241
|
+
out.set(
|
|
229
242
|
row.space,
|
|
230
243
|
parseMarkerRowSafely(row, parseContractMarkerRow, {
|
|
231
244
|
space: row.space,
|
|
@@ -233,7 +246,271 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
233
246
|
}),
|
|
234
247
|
);
|
|
235
248
|
}
|
|
236
|
-
return
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Reads per-migration ledger rows from `prisma_contract.ledger` in apply
|
|
254
|
+
* order. Probes `information_schema.tables` first so a fresh database
|
|
255
|
+
* without the ledger table returns `[]` instead of raising "relation does
|
|
256
|
+
* not exist".
|
|
257
|
+
*/
|
|
258
|
+
async readLedger(
|
|
259
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
260
|
+
space?: string,
|
|
261
|
+
): Promise<readonly LedgerEntryRecord[]> {
|
|
262
|
+
const ledgerContext = { space: space ?? '*', markerLocation: POSTGRES_LEDGER_TABLE };
|
|
263
|
+
return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), ledgerContext);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async readLedgerResult(
|
|
267
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
268
|
+
space: string | undefined,
|
|
269
|
+
): Promise<readonly LedgerEntryRecord[]> {
|
|
270
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
271
|
+
const probe = infoSchemaTables
|
|
272
|
+
.select(infoSchemaTables.table_schema)
|
|
273
|
+
.where(
|
|
274
|
+
infoSchemaTables.table_schema
|
|
275
|
+
.eq('prisma_contract')
|
|
276
|
+
.and(infoSchemaTables.table_name.eq('ledger')),
|
|
277
|
+
)
|
|
278
|
+
.build();
|
|
279
|
+
const exists = await execute(lower, driver, probe);
|
|
280
|
+
if (exists.length === 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const base = ledgerReadShape.select(
|
|
285
|
+
ledgerReadShape.space,
|
|
286
|
+
ledgerReadShape.migration_name,
|
|
287
|
+
ledgerReadShape.migration_hash,
|
|
288
|
+
ledgerReadShape.origin_core_hash,
|
|
289
|
+
ledgerReadShape.destination_core_hash,
|
|
290
|
+
ledgerReadShape.operations,
|
|
291
|
+
ledgerReadShape.created_at,
|
|
292
|
+
);
|
|
293
|
+
const filtered = space !== undefined ? base.where(ledgerReadShape.space.eq(space)) : base;
|
|
294
|
+
const rawRows = await execute(lower, driver, filtered.orderBy(ledgerReadShape.id).build());
|
|
295
|
+
const rows = blindCast<readonly PostgresLedgerRow[], 'Driver returns rows shaped by SELECT'>(
|
|
296
|
+
rawRows,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return rows.map((row) => {
|
|
300
|
+
const appliedAt = row.created_at instanceof Date ? row.created_at : new Date(row.created_at);
|
|
301
|
+
return {
|
|
302
|
+
space: row.space,
|
|
303
|
+
migrationName: row.migration_name,
|
|
304
|
+
migrationHash: row.migration_hash,
|
|
305
|
+
from: ledgerOriginFromStored(row.origin_core_hash),
|
|
306
|
+
to: row.destination_core_hash,
|
|
307
|
+
appliedAt,
|
|
308
|
+
operationCount: Array.isArray(row.operations) ? row.operations.length : 0,
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Stamps the initial marker row for `space` via the shared contract-free DML
|
|
315
|
+
* builder, lowered through {@link lower} and executed on the driver. See the
|
|
316
|
+
* `SqlControlAdapter.initMarker` contract.
|
|
317
|
+
*/
|
|
318
|
+
async insertMarker(
|
|
319
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
320
|
+
space: string,
|
|
321
|
+
destination: {
|
|
322
|
+
readonly storageHash: string;
|
|
323
|
+
readonly profileHash: string;
|
|
324
|
+
readonly invariants?: readonly string[];
|
|
325
|
+
},
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
await execute(
|
|
328
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
329
|
+
driver,
|
|
330
|
+
marker
|
|
331
|
+
.insert({
|
|
332
|
+
space,
|
|
333
|
+
core_hash: destination.storageHash,
|
|
334
|
+
profile_hash: destination.profileHash,
|
|
335
|
+
contract_json: null,
|
|
336
|
+
canonical_version: null,
|
|
337
|
+
updated_at: NOW,
|
|
338
|
+
app_tag: null,
|
|
339
|
+
meta: {},
|
|
340
|
+
invariants: destination.invariants ?? [],
|
|
341
|
+
})
|
|
342
|
+
.build(),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async initMarker(
|
|
347
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
348
|
+
space: string,
|
|
349
|
+
destination: {
|
|
350
|
+
readonly storageHash: string;
|
|
351
|
+
readonly profileHash: string;
|
|
352
|
+
readonly invariants?: readonly string[];
|
|
353
|
+
},
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
await execute(
|
|
356
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
357
|
+
driver,
|
|
358
|
+
marker
|
|
359
|
+
.upsert({
|
|
360
|
+
space,
|
|
361
|
+
core_hash: destination.storageHash,
|
|
362
|
+
profile_hash: destination.profileHash,
|
|
363
|
+
contract_json: null,
|
|
364
|
+
canonical_version: null,
|
|
365
|
+
updated_at: NOW,
|
|
366
|
+
app_tag: null,
|
|
367
|
+
meta: {},
|
|
368
|
+
invariants: destination.invariants ?? [],
|
|
369
|
+
})
|
|
370
|
+
.onConflict(marker.space)
|
|
371
|
+
.doUpdate((excluded) => ({
|
|
372
|
+
core_hash: excluded.core_hash,
|
|
373
|
+
profile_hash: excluded.profile_hash,
|
|
374
|
+
contract_json: excluded.contract_json,
|
|
375
|
+
canonical_version: excluded.canonical_version,
|
|
376
|
+
updated_at: NOW,
|
|
377
|
+
app_tag: excluded.app_tag,
|
|
378
|
+
meta: excluded.meta,
|
|
379
|
+
invariants: excluded.invariants,
|
|
380
|
+
}))
|
|
381
|
+
.build(),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Compare-and-swap advance of the marker row for `space`. See the
|
|
387
|
+
* `SqlControlAdapter.updateMarker` contract.
|
|
388
|
+
*/
|
|
389
|
+
async updateMarker(
|
|
390
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
391
|
+
space: string,
|
|
392
|
+
expectedFrom: string,
|
|
393
|
+
destination: {
|
|
394
|
+
readonly storageHash: string;
|
|
395
|
+
readonly profileHash: string;
|
|
396
|
+
readonly invariants?: readonly string[];
|
|
397
|
+
},
|
|
398
|
+
): Promise<boolean> {
|
|
399
|
+
const currentInvariants =
|
|
400
|
+
destination.invariants === undefined
|
|
401
|
+
? []
|
|
402
|
+
: ((await this.readMarker(driver, space))?.invariants ?? []);
|
|
403
|
+
const mergedInvariants =
|
|
404
|
+
destination.invariants === undefined
|
|
405
|
+
? undefined
|
|
406
|
+
: mergeInvariants(currentInvariants, destination.invariants);
|
|
407
|
+
|
|
408
|
+
const query = marker
|
|
409
|
+
.update()
|
|
410
|
+
.set({
|
|
411
|
+
core_hash: destination.storageHash,
|
|
412
|
+
profile_hash: destination.profileHash,
|
|
413
|
+
updated_at: NOW,
|
|
414
|
+
...(mergedInvariants !== undefined ? { invariants: mergedInvariants } : {}),
|
|
415
|
+
})
|
|
416
|
+
.where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
|
|
417
|
+
.returning(marker.space)
|
|
418
|
+
.build();
|
|
419
|
+
|
|
420
|
+
const rows = await execute((q) => this.lower(q, { contract: undefined }), driver, query);
|
|
421
|
+
return rows.length > 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Appends a ledger entry for `space`. See the
|
|
426
|
+
* `SqlControlAdapter.writeLedgerEntry` contract.
|
|
427
|
+
*/
|
|
428
|
+
async writeLedgerEntry(
|
|
429
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
430
|
+
space: string,
|
|
431
|
+
entry: {
|
|
432
|
+
readonly edgeId: string;
|
|
433
|
+
readonly from: string;
|
|
434
|
+
readonly to: string;
|
|
435
|
+
readonly migrationName: string;
|
|
436
|
+
readonly migrationHash: string;
|
|
437
|
+
readonly operations: readonly unknown[];
|
|
438
|
+
},
|
|
439
|
+
): Promise<void> {
|
|
440
|
+
await execute(
|
|
441
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
442
|
+
driver,
|
|
443
|
+
ledger
|
|
444
|
+
.insert({
|
|
445
|
+
space,
|
|
446
|
+
migration_name: entry.migrationName,
|
|
447
|
+
migration_hash: entry.migrationHash,
|
|
448
|
+
origin_core_hash: entry.from,
|
|
449
|
+
destination_core_hash: entry.to,
|
|
450
|
+
operations: entry.operations,
|
|
451
|
+
})
|
|
452
|
+
.build(),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private async assertMarkerTableHasSpaceColumn(
|
|
457
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
458
|
+
space: string,
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
const result = await driver.query<{ column_name: string }>(
|
|
461
|
+
`select column_name
|
|
462
|
+
from information_schema.columns
|
|
463
|
+
where table_schema = 'prisma_contract'
|
|
464
|
+
and table_name = 'marker'`,
|
|
465
|
+
);
|
|
466
|
+
const rows = result.rows;
|
|
467
|
+
if (rows.length === 0) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!rows.every((row) => typeof row.column_name === 'string')) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (rows.some((row) => row.column_name === 'space')) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
rethrowMarkerReadError(new Error('column "space" does not exist'), {
|
|
477
|
+
space,
|
|
478
|
+
markerLocation: POSTGRES_MARKER_TABLE,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private async readMarkerResult(driver: SqlControlDriverInstance<'postgres'>, space: string) {
|
|
483
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
484
|
+
const probe = infoSchemaTables
|
|
485
|
+
.select(infoSchemaTables.table_schema)
|
|
486
|
+
.where(
|
|
487
|
+
infoSchemaTables.table_schema
|
|
488
|
+
.eq('prisma_contract')
|
|
489
|
+
.and(infoSchemaTables.table_name.eq('marker')),
|
|
490
|
+
)
|
|
491
|
+
.build();
|
|
492
|
+
const exists = await execute(lower, driver, probe);
|
|
493
|
+
if (exists.length === 0) return { kind: 'no-table' as const };
|
|
494
|
+
|
|
495
|
+
await this.assertMarkerTableHasSpaceColumn(driver, space);
|
|
496
|
+
|
|
497
|
+
const fetch = marker
|
|
498
|
+
.select(
|
|
499
|
+
marker.core_hash,
|
|
500
|
+
marker.profile_hash,
|
|
501
|
+
marker.contract_json,
|
|
502
|
+
marker.canonical_version,
|
|
503
|
+
marker.updated_at,
|
|
504
|
+
marker.app_tag,
|
|
505
|
+
marker.meta,
|
|
506
|
+
marker.invariants,
|
|
507
|
+
)
|
|
508
|
+
.where(marker.space.eq(space))
|
|
509
|
+
.build();
|
|
510
|
+
const result = await execute(lower, driver, fetch);
|
|
511
|
+
const row = result[0];
|
|
512
|
+
if (!row) return { kind: 'absent' as const };
|
|
513
|
+
return { kind: 'present' as const, record: parseContractMarkerRow(row) };
|
|
237
514
|
}
|
|
238
515
|
|
|
239
516
|
/**
|
|
@@ -255,13 +532,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
255
532
|
* Uses batched queries to minimize database round trips (6 queries per
|
|
256
533
|
* schema walked).
|
|
257
534
|
*
|
|
258
|
-
* @param driver -
|
|
535
|
+
* @param driver - SqlControlDriverInstance<'postgres'> instance for executing queries
|
|
259
536
|
* @param contract - Optional contract for contract-guided introspection (multi-namespace walk, filtering)
|
|
260
537
|
* @param schema - Schema name to introspect when no contract is provided (defaults to 'public')
|
|
261
538
|
* @returns Promise resolving to SqlSchemaIR representing the live database schema
|
|
262
539
|
*/
|
|
263
540
|
async introspect(
|
|
264
|
-
driver:
|
|
541
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
265
542
|
contract?: unknown,
|
|
266
543
|
schema = 'public',
|
|
267
544
|
): Promise<SqlSchemaIR> {
|
|
@@ -295,7 +572,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
295
572
|
* `CREATE SCHEMA` before table DDL.
|
|
296
573
|
*/
|
|
297
574
|
private async listExistingSchemas(
|
|
298
|
-
driver:
|
|
575
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
299
576
|
): Promise<readonly string[]> {
|
|
300
577
|
const result = await driver.query<{ nspname: string }>(
|
|
301
578
|
`SELECT nspname
|
|
@@ -316,7 +593,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
316
593
|
* table regardless of which namespace it lives in.
|
|
317
594
|
*/
|
|
318
595
|
private async introspectNamespaces(
|
|
319
|
-
driver:
|
|
596
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
320
597
|
namespaceIds: readonly string[],
|
|
321
598
|
): Promise<SqlSchemaIR> {
|
|
322
599
|
const resolvedSchemas = await Promise.all(
|
|
@@ -380,7 +657,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
380
657
|
* the per-namespace walk.
|
|
381
658
|
*/
|
|
382
659
|
private async introspectSchema(
|
|
383
|
-
driver:
|
|
660
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
384
661
|
schema: string,
|
|
385
662
|
): Promise<SqlSchemaIR> {
|
|
386
663
|
// Execute all queries in parallel for efficiency (6 queries instead of 5T+1)
|
|
@@ -793,9 +1070,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
793
1070
|
/**
|
|
794
1071
|
* Gets the Postgres version from the database.
|
|
795
1072
|
*/
|
|
796
|
-
private async getPostgresVersion(
|
|
797
|
-
driver: ControlDriverInstance<'sql', 'postgres'>,
|
|
798
|
-
): Promise<string> {
|
|
1073
|
+
private async getPostgresVersion(driver: SqlControlDriverInstance<'postgres'>): Promise<string> {
|
|
799
1074
|
const result = await driver.query<{ version: string }>('SELECT version() AS version', []);
|
|
800
1075
|
const versionString = result.rows[0]?.version ?? '';
|
|
801
1076
|
// Extract version number from "PostgreSQL 15.1 ..." format
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DdlColumn,
|
|
3
|
+
DdlColumnDefaultVisitor,
|
|
4
|
+
FunctionColumnDefault,
|
|
5
|
+
LiteralColumnDefault,
|
|
6
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
7
|
+
import type {
|
|
8
|
+
PostgresCreateSchema,
|
|
9
|
+
PostgresCreateTable,
|
|
10
|
+
PostgresDdlNode,
|
|
11
|
+
PostgresDdlVisitor,
|
|
12
|
+
} from '@prisma-next/target-postgres/ddl';
|
|
13
|
+
import { escapeLiteral } from '@prisma-next/target-postgres/sql-utils';
|
|
14
|
+
import type { PostgresLoweredStatement } from './types';
|
|
15
|
+
|
|
16
|
+
class PostgresDdlVisitorImpl implements PostgresDdlVisitor<string> {
|
|
17
|
+
createTable(node: PostgresCreateTable): string {
|
|
18
|
+
const ifNotExists = node.ifNotExists ? 'if not exists ' : '';
|
|
19
|
+
const tableRef = node.schema ? `${node.schema}.${node.table}` : node.table;
|
|
20
|
+
const columnDefs = node.columns.map((column) => renderColumn(column)).join(',\n ');
|
|
21
|
+
return `create table ${ifNotExists}${tableRef} (\n ${columnDefs}\n )`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
createSchema(node: PostgresCreateSchema): string {
|
|
25
|
+
const ifNotExists = node.ifNotExists ? 'if not exists ' : '';
|
|
26
|
+
return `create schema ${ifNotExists}${node.schema}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultVisitor: DdlColumnDefaultVisitor<string> = {
|
|
31
|
+
literal(node: LiteralColumnDefault): string {
|
|
32
|
+
const { value } = node;
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
return `default '${escapeLiteral(value)}'`;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
37
|
+
return `default ${String(value)}`;
|
|
38
|
+
}
|
|
39
|
+
if (value === null) {
|
|
40
|
+
return 'default null';
|
|
41
|
+
}
|
|
42
|
+
return `default '${JSON.stringify(value)}'`;
|
|
43
|
+
},
|
|
44
|
+
function(node: FunctionColumnDefault): string {
|
|
45
|
+
if (node.expression === 'autoincrement()') {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
if (node.expression === 'now()') {
|
|
49
|
+
return 'default now()';
|
|
50
|
+
}
|
|
51
|
+
return `default (${node.expression})`;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function renderColumn(column: DdlColumn): string {
|
|
56
|
+
const parts = [column.name, column.type];
|
|
57
|
+
if (column.notNull) {
|
|
58
|
+
parts.push('not null');
|
|
59
|
+
}
|
|
60
|
+
if (column.primaryKey) {
|
|
61
|
+
parts.push('primary key');
|
|
62
|
+
}
|
|
63
|
+
const defaultClause = column.default ? column.default.accept(defaultVisitor) : '';
|
|
64
|
+
if (defaultClause.length > 0) {
|
|
65
|
+
parts.push(defaultClause);
|
|
66
|
+
}
|
|
67
|
+
return parts.join(' ');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function renderLoweredDdl(ast: PostgresDdlNode): PostgresLoweredStatement {
|
|
71
|
+
const sql = ast.accept(new PostgresDdlVisitorImpl());
|
|
72
|
+
return Object.freeze({ sql, params: Object.freeze([]) });
|
|
73
|
+
}
|
|
@@ -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';
|