@prisma-next/adapter-postgres 0.12.0-dev.7 → 0.12.0-dev.70
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-BpvkORJG.mjs} +13 -13
- package/dist/adapter-BpvkORJG.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-BmVmHzER.mjs +1483 -0
- package/dist/control-adapter-BmVmHzER.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 +615 -193
- 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 +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,21 +1,35 @@
|
|
|
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,
|
|
32
|
+
SqlCheckConstraintIRInput,
|
|
19
33
|
SqlColumnIR,
|
|
20
34
|
SqlForeignKeyIR,
|
|
21
35
|
SqlIndexIR,
|
|
@@ -24,6 +38,11 @@ import type {
|
|
|
24
38
|
SqlTableIR,
|
|
25
39
|
SqlUniqueIR,
|
|
26
40
|
} from '@prisma-next/sql-schema-ir/types';
|
|
41
|
+
import {
|
|
42
|
+
buildControlTableBootstrapQueries,
|
|
43
|
+
buildSignMarkerBootstrapQueries,
|
|
44
|
+
} from '@prisma-next/target-postgres/contract-free';
|
|
45
|
+
import type { PostgresDdlNode } from '@prisma-next/target-postgres/ddl';
|
|
27
46
|
import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
|
|
28
47
|
import {
|
|
29
48
|
createResolveExistingEnumValues,
|
|
@@ -35,14 +54,35 @@ import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-t
|
|
|
35
54
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
36
55
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
37
56
|
import { createPostgresBuiltinCodecLookup } from './codec-lookup';
|
|
57
|
+
import { renderLoweredDdl } from './ddl-renderer';
|
|
38
58
|
import {
|
|
39
59
|
introspectPostgresEnumTypes,
|
|
40
60
|
type PostgresEnumStorageTypeAnnotation,
|
|
41
61
|
} from './enum-control-hooks';
|
|
62
|
+
import {
|
|
63
|
+
execute,
|
|
64
|
+
infoSchemaTables,
|
|
65
|
+
ledger,
|
|
66
|
+
ledgerReadShape,
|
|
67
|
+
marker,
|
|
68
|
+
mergeInvariants,
|
|
69
|
+
NOW,
|
|
70
|
+
} from './marker-ledger';
|
|
42
71
|
import { renderLoweredSql } from './sql-renderer';
|
|
43
72
|
import type { PostgresContract } from './types';
|
|
44
73
|
|
|
45
74
|
const POSTGRES_MARKER_TABLE = 'prisma_contract.marker';
|
|
75
|
+
const POSTGRES_LEDGER_TABLE = 'prisma_contract.ledger';
|
|
76
|
+
|
|
77
|
+
type PostgresLedgerRow = {
|
|
78
|
+
readonly space: string;
|
|
79
|
+
readonly migration_name: string;
|
|
80
|
+
readonly migration_hash: string;
|
|
81
|
+
readonly origin_core_hash: string | null;
|
|
82
|
+
readonly destination_core_hash: string;
|
|
83
|
+
readonly operations: unknown;
|
|
84
|
+
readonly created_at: Date | string;
|
|
85
|
+
};
|
|
46
86
|
|
|
47
87
|
/**
|
|
48
88
|
* Postgres control plane adapter for control-plane operations like introspection.
|
|
@@ -99,6 +139,14 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
99
139
|
readonly resolveExistingEnumValuesForContract = (contract: Contract<SqlStorage>) =>
|
|
100
140
|
createResolveExistingEnumValues(contract.storage);
|
|
101
141
|
|
|
142
|
+
bootstrapControlTableQueries(): readonly DdlNode[] {
|
|
143
|
+
return buildControlTableBootstrapQueries();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
bootstrapSignMarkerQueries(): readonly DdlNode[] {
|
|
147
|
+
return buildSignMarkerBootstrapQueries();
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
/**
|
|
103
151
|
* Lower a SQL query AST into a Postgres-flavored `{ sql, params }` payload.
|
|
104
152
|
*
|
|
@@ -107,7 +155,10 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
107
155
|
* and contract. Used at migration plan/emit time (e.g. by `dataTransform`)
|
|
108
156
|
* without instantiating the runtime adapter.
|
|
109
157
|
*/
|
|
110
|
-
lower(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
|
|
158
|
+
lower(ast: AnyQueryAst | PostgresDdlNode, context: LowererContext<unknown>): LoweredStatement {
|
|
159
|
+
if (isDdlNode(ast)) {
|
|
160
|
+
return renderLoweredDdl(ast);
|
|
161
|
+
}
|
|
111
162
|
return renderLoweredSql(ast, context.contract as PostgresContract, this.codecLookup);
|
|
112
163
|
}
|
|
113
164
|
|
|
@@ -120,55 +171,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
120
171
|
* parse errors, so we probe before reading.
|
|
121
172
|
*/
|
|
122
173
|
async readMarker(
|
|
123
|
-
driver:
|
|
174
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
124
175
|
space: string,
|
|
125
176
|
): 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
|
-
);
|
|
177
|
+
const result = await this.readMarkerDiscriminated(driver, space);
|
|
178
|
+
return result.kind === 'present' ? result.record : null;
|
|
179
|
+
}
|
|
168
180
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
async readMarkerDiscriminated(
|
|
182
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
183
|
+
space: string,
|
|
184
|
+
): Promise<MarkerReadResult> {
|
|
185
|
+
const markerContext = { space, markerLocation: POSTGRES_MARKER_TABLE };
|
|
186
|
+
return withMarkerReadErrorHandling(() => this.readMarkerResult(driver, space), markerContext);
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
/**
|
|
@@ -178,54 +193,53 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
178
193
|
* map rather than raising "relation does not exist".
|
|
179
194
|
*/
|
|
180
195
|
async readAllMarkers(
|
|
181
|
-
driver:
|
|
196
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
182
197
|
): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
183
198
|
const markerContext = { space: APP_SPACE_ID, markerLocation: POSTGRES_MARKER_TABLE };
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
return withMarkerReadErrorHandling(() => this.readAllMarkersResult(driver), markerContext);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async readAllMarkersResult(
|
|
203
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
204
|
+
): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
205
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
206
|
+
const probe = infoSchemaTables
|
|
207
|
+
.select(infoSchemaTables.table_schema)
|
|
208
|
+
.where(
|
|
209
|
+
infoSchemaTables.table_schema
|
|
210
|
+
.eq('prisma_contract')
|
|
211
|
+
.and(infoSchemaTables.table_name.eq('marker')),
|
|
212
|
+
)
|
|
213
|
+
.build();
|
|
214
|
+
const exists = await execute(lower, driver, probe);
|
|
215
|
+
if (exists.length === 0) {
|
|
195
216
|
return new Map();
|
|
196
217
|
}
|
|
197
218
|
|
|
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
|
-
);
|
|
219
|
+
await this.assertMarkerTableHasSpaceColumn(driver, APP_SPACE_ID);
|
|
225
220
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
const fetch = marker
|
|
222
|
+
.select(
|
|
223
|
+
marker.space,
|
|
224
|
+
marker.core_hash,
|
|
225
|
+
marker.profile_hash,
|
|
226
|
+
marker.contract_json,
|
|
227
|
+
marker.canonical_version,
|
|
228
|
+
marker.updated_at,
|
|
229
|
+
marker.app_tag,
|
|
230
|
+
marker.meta,
|
|
231
|
+
marker.invariants,
|
|
232
|
+
)
|
|
233
|
+
.build();
|
|
234
|
+
const rawRows = await execute(lower, driver, fetch);
|
|
235
|
+
const rows = blindCast<
|
|
236
|
+
ReadonlyArray<{ space: string } & Record<string, unknown>>,
|
|
237
|
+
'Driver returns rows shaped by SELECT'
|
|
238
|
+
>(rawRows);
|
|
239
|
+
|
|
240
|
+
const out = new Map<string, ContractMarkerRecord>();
|
|
241
|
+
for (const row of rows) {
|
|
242
|
+
out.set(
|
|
229
243
|
row.space,
|
|
230
244
|
parseMarkerRowSafely(row, parseContractMarkerRow, {
|
|
231
245
|
space: row.space,
|
|
@@ -233,7 +247,271 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
233
247
|
}),
|
|
234
248
|
);
|
|
235
249
|
}
|
|
236
|
-
return
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Reads per-migration ledger rows from `prisma_contract.ledger` in apply
|
|
255
|
+
* order. Probes `information_schema.tables` first so a fresh database
|
|
256
|
+
* without the ledger table returns `[]` instead of raising "relation does
|
|
257
|
+
* not exist".
|
|
258
|
+
*/
|
|
259
|
+
async readLedger(
|
|
260
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
261
|
+
space?: string,
|
|
262
|
+
): Promise<readonly LedgerEntryRecord[]> {
|
|
263
|
+
const ledgerContext = { space: space ?? '*', markerLocation: POSTGRES_LEDGER_TABLE };
|
|
264
|
+
return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), ledgerContext);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async readLedgerResult(
|
|
268
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
269
|
+
space: string | undefined,
|
|
270
|
+
): Promise<readonly LedgerEntryRecord[]> {
|
|
271
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
272
|
+
const probe = infoSchemaTables
|
|
273
|
+
.select(infoSchemaTables.table_schema)
|
|
274
|
+
.where(
|
|
275
|
+
infoSchemaTables.table_schema
|
|
276
|
+
.eq('prisma_contract')
|
|
277
|
+
.and(infoSchemaTables.table_name.eq('ledger')),
|
|
278
|
+
)
|
|
279
|
+
.build();
|
|
280
|
+
const exists = await execute(lower, driver, probe);
|
|
281
|
+
if (exists.length === 0) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const base = ledgerReadShape.select(
|
|
286
|
+
ledgerReadShape.space,
|
|
287
|
+
ledgerReadShape.migration_name,
|
|
288
|
+
ledgerReadShape.migration_hash,
|
|
289
|
+
ledgerReadShape.origin_core_hash,
|
|
290
|
+
ledgerReadShape.destination_core_hash,
|
|
291
|
+
ledgerReadShape.operations,
|
|
292
|
+
ledgerReadShape.created_at,
|
|
293
|
+
);
|
|
294
|
+
const filtered = space !== undefined ? base.where(ledgerReadShape.space.eq(space)) : base;
|
|
295
|
+
const rawRows = await execute(lower, driver, filtered.orderBy(ledgerReadShape.id).build());
|
|
296
|
+
const rows = blindCast<readonly PostgresLedgerRow[], 'Driver returns rows shaped by SELECT'>(
|
|
297
|
+
rawRows,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return rows.map((row) => {
|
|
301
|
+
const appliedAt = row.created_at instanceof Date ? row.created_at : new Date(row.created_at);
|
|
302
|
+
return {
|
|
303
|
+
space: row.space,
|
|
304
|
+
migrationName: row.migration_name,
|
|
305
|
+
migrationHash: row.migration_hash,
|
|
306
|
+
from: ledgerOriginFromStored(row.origin_core_hash),
|
|
307
|
+
to: row.destination_core_hash,
|
|
308
|
+
appliedAt,
|
|
309
|
+
operationCount: Array.isArray(row.operations) ? row.operations.length : 0,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Stamps the initial marker row for `space` via the shared contract-free DML
|
|
316
|
+
* builder, lowered through {@link lower} and executed on the driver. See the
|
|
317
|
+
* `SqlControlAdapter.initMarker` contract.
|
|
318
|
+
*/
|
|
319
|
+
async insertMarker(
|
|
320
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
321
|
+
space: string,
|
|
322
|
+
destination: {
|
|
323
|
+
readonly storageHash: string;
|
|
324
|
+
readonly profileHash: string;
|
|
325
|
+
readonly invariants?: readonly string[];
|
|
326
|
+
},
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
await execute(
|
|
329
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
330
|
+
driver,
|
|
331
|
+
marker
|
|
332
|
+
.insert({
|
|
333
|
+
space,
|
|
334
|
+
core_hash: destination.storageHash,
|
|
335
|
+
profile_hash: destination.profileHash,
|
|
336
|
+
contract_json: null,
|
|
337
|
+
canonical_version: null,
|
|
338
|
+
updated_at: NOW,
|
|
339
|
+
app_tag: null,
|
|
340
|
+
meta: {},
|
|
341
|
+
invariants: destination.invariants ?? [],
|
|
342
|
+
})
|
|
343
|
+
.build(),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async initMarker(
|
|
348
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
349
|
+
space: string,
|
|
350
|
+
destination: {
|
|
351
|
+
readonly storageHash: string;
|
|
352
|
+
readonly profileHash: string;
|
|
353
|
+
readonly invariants?: readonly string[];
|
|
354
|
+
},
|
|
355
|
+
): Promise<void> {
|
|
356
|
+
await execute(
|
|
357
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
358
|
+
driver,
|
|
359
|
+
marker
|
|
360
|
+
.upsert({
|
|
361
|
+
space,
|
|
362
|
+
core_hash: destination.storageHash,
|
|
363
|
+
profile_hash: destination.profileHash,
|
|
364
|
+
contract_json: null,
|
|
365
|
+
canonical_version: null,
|
|
366
|
+
updated_at: NOW,
|
|
367
|
+
app_tag: null,
|
|
368
|
+
meta: {},
|
|
369
|
+
invariants: destination.invariants ?? [],
|
|
370
|
+
})
|
|
371
|
+
.onConflict(marker.space)
|
|
372
|
+
.doUpdate((excluded) => ({
|
|
373
|
+
core_hash: excluded.core_hash,
|
|
374
|
+
profile_hash: excluded.profile_hash,
|
|
375
|
+
contract_json: excluded.contract_json,
|
|
376
|
+
canonical_version: excluded.canonical_version,
|
|
377
|
+
updated_at: NOW,
|
|
378
|
+
app_tag: excluded.app_tag,
|
|
379
|
+
meta: excluded.meta,
|
|
380
|
+
invariants: excluded.invariants,
|
|
381
|
+
}))
|
|
382
|
+
.build(),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Compare-and-swap advance of the marker row for `space`. See the
|
|
388
|
+
* `SqlControlAdapter.updateMarker` contract.
|
|
389
|
+
*/
|
|
390
|
+
async updateMarker(
|
|
391
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
392
|
+
space: string,
|
|
393
|
+
expectedFrom: string,
|
|
394
|
+
destination: {
|
|
395
|
+
readonly storageHash: string;
|
|
396
|
+
readonly profileHash: string;
|
|
397
|
+
readonly invariants?: readonly string[];
|
|
398
|
+
},
|
|
399
|
+
): Promise<boolean> {
|
|
400
|
+
const currentInvariants =
|
|
401
|
+
destination.invariants === undefined
|
|
402
|
+
? []
|
|
403
|
+
: ((await this.readMarker(driver, space))?.invariants ?? []);
|
|
404
|
+
const mergedInvariants =
|
|
405
|
+
destination.invariants === undefined
|
|
406
|
+
? undefined
|
|
407
|
+
: mergeInvariants(currentInvariants, destination.invariants);
|
|
408
|
+
|
|
409
|
+
const query = marker
|
|
410
|
+
.update()
|
|
411
|
+
.set({
|
|
412
|
+
core_hash: destination.storageHash,
|
|
413
|
+
profile_hash: destination.profileHash,
|
|
414
|
+
updated_at: NOW,
|
|
415
|
+
...(mergedInvariants !== undefined ? { invariants: mergedInvariants } : {}),
|
|
416
|
+
})
|
|
417
|
+
.where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
|
|
418
|
+
.returning(marker.space)
|
|
419
|
+
.build();
|
|
420
|
+
|
|
421
|
+
const rows = await execute((q) => this.lower(q, { contract: undefined }), driver, query);
|
|
422
|
+
return rows.length > 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Appends a ledger entry for `space`. See the
|
|
427
|
+
* `SqlControlAdapter.writeLedgerEntry` contract.
|
|
428
|
+
*/
|
|
429
|
+
async writeLedgerEntry(
|
|
430
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
431
|
+
space: string,
|
|
432
|
+
entry: {
|
|
433
|
+
readonly edgeId: string;
|
|
434
|
+
readonly from: string;
|
|
435
|
+
readonly to: string;
|
|
436
|
+
readonly migrationName: string;
|
|
437
|
+
readonly migrationHash: string;
|
|
438
|
+
readonly operations: readonly unknown[];
|
|
439
|
+
},
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
await execute(
|
|
442
|
+
(query) => this.lower(query, { contract: undefined }),
|
|
443
|
+
driver,
|
|
444
|
+
ledger
|
|
445
|
+
.insert({
|
|
446
|
+
space,
|
|
447
|
+
migration_name: entry.migrationName,
|
|
448
|
+
migration_hash: entry.migrationHash,
|
|
449
|
+
origin_core_hash: entry.from,
|
|
450
|
+
destination_core_hash: entry.to,
|
|
451
|
+
operations: entry.operations,
|
|
452
|
+
})
|
|
453
|
+
.build(),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async assertMarkerTableHasSpaceColumn(
|
|
458
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
459
|
+
space: string,
|
|
460
|
+
): Promise<void> {
|
|
461
|
+
const result = await driver.query<{ column_name: string }>(
|
|
462
|
+
`select column_name
|
|
463
|
+
from information_schema.columns
|
|
464
|
+
where table_schema = 'prisma_contract'
|
|
465
|
+
and table_name = 'marker'`,
|
|
466
|
+
);
|
|
467
|
+
const rows = result.rows;
|
|
468
|
+
if (rows.length === 0) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!rows.every((row) => typeof row.column_name === 'string')) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (rows.some((row) => row.column_name === 'space')) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
rethrowMarkerReadError(new Error('column "space" does not exist'), {
|
|
478
|
+
space,
|
|
479
|
+
markerLocation: POSTGRES_MARKER_TABLE,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private async readMarkerResult(driver: SqlControlDriverInstance<'postgres'>, space: string) {
|
|
484
|
+
const lower = (query: AnyQueryAst) => this.lower(query, { contract: undefined });
|
|
485
|
+
const probe = infoSchemaTables
|
|
486
|
+
.select(infoSchemaTables.table_schema)
|
|
487
|
+
.where(
|
|
488
|
+
infoSchemaTables.table_schema
|
|
489
|
+
.eq('prisma_contract')
|
|
490
|
+
.and(infoSchemaTables.table_name.eq('marker')),
|
|
491
|
+
)
|
|
492
|
+
.build();
|
|
493
|
+
const exists = await execute(lower, driver, probe);
|
|
494
|
+
if (exists.length === 0) return { kind: 'no-table' as const };
|
|
495
|
+
|
|
496
|
+
await this.assertMarkerTableHasSpaceColumn(driver, space);
|
|
497
|
+
|
|
498
|
+
const fetch = marker
|
|
499
|
+
.select(
|
|
500
|
+
marker.core_hash,
|
|
501
|
+
marker.profile_hash,
|
|
502
|
+
marker.contract_json,
|
|
503
|
+
marker.canonical_version,
|
|
504
|
+
marker.updated_at,
|
|
505
|
+
marker.app_tag,
|
|
506
|
+
marker.meta,
|
|
507
|
+
marker.invariants,
|
|
508
|
+
)
|
|
509
|
+
.where(marker.space.eq(space))
|
|
510
|
+
.build();
|
|
511
|
+
const result = await execute(lower, driver, fetch);
|
|
512
|
+
const row = result[0];
|
|
513
|
+
if (!row) return { kind: 'absent' as const };
|
|
514
|
+
return { kind: 'present' as const, record: parseContractMarkerRow(row) };
|
|
237
515
|
}
|
|
238
516
|
|
|
239
517
|
/**
|
|
@@ -255,13 +533,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
255
533
|
* Uses batched queries to minimize database round trips (6 queries per
|
|
256
534
|
* schema walked).
|
|
257
535
|
*
|
|
258
|
-
* @param driver -
|
|
536
|
+
* @param driver - SqlControlDriverInstance<'postgres'> instance for executing queries
|
|
259
537
|
* @param contract - Optional contract for contract-guided introspection (multi-namespace walk, filtering)
|
|
260
538
|
* @param schema - Schema name to introspect when no contract is provided (defaults to 'public')
|
|
261
539
|
* @returns Promise resolving to SqlSchemaIR representing the live database schema
|
|
262
540
|
*/
|
|
263
541
|
async introspect(
|
|
264
|
-
driver:
|
|
542
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
265
543
|
contract?: unknown,
|
|
266
544
|
schema = 'public',
|
|
267
545
|
): Promise<SqlSchemaIR> {
|
|
@@ -295,7 +573,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
295
573
|
* `CREATE SCHEMA` before table DDL.
|
|
296
574
|
*/
|
|
297
575
|
private async listExistingSchemas(
|
|
298
|
-
driver:
|
|
576
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
299
577
|
): Promise<readonly string[]> {
|
|
300
578
|
const result = await driver.query<{ nspname: string }>(
|
|
301
579
|
`SELECT nspname
|
|
@@ -316,7 +594,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
316
594
|
* table regardless of which namespace it lives in.
|
|
317
595
|
*/
|
|
318
596
|
private async introspectNamespaces(
|
|
319
|
-
driver:
|
|
597
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
320
598
|
namespaceIds: readonly string[],
|
|
321
599
|
): Promise<SqlSchemaIR> {
|
|
322
600
|
const resolvedSchemas = await Promise.all(
|
|
@@ -380,35 +658,42 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
380
658
|
* the per-namespace walk.
|
|
381
659
|
*/
|
|
382
660
|
private async introspectSchema(
|
|
383
|
-
driver:
|
|
661
|
+
driver: SqlControlDriverInstance<'postgres'>,
|
|
384
662
|
schema: string,
|
|
385
663
|
): Promise<SqlSchemaIR> {
|
|
386
|
-
// Execute all queries in parallel for efficiency (
|
|
387
|
-
const [
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
664
|
+
// Execute all queries in parallel for efficiency (7 queries instead of 6T+1)
|
|
665
|
+
const [
|
|
666
|
+
tablesResult,
|
|
667
|
+
columnsResult,
|
|
668
|
+
pkResult,
|
|
669
|
+
fkResult,
|
|
670
|
+
uniqueResult,
|
|
671
|
+
indexResult,
|
|
672
|
+
checkResult,
|
|
673
|
+
] = await Promise.all([
|
|
674
|
+
// Query all tables
|
|
675
|
+
driver.query<{ table_name: string }>(
|
|
676
|
+
`SELECT table_name
|
|
392
677
|
FROM information_schema.tables
|
|
393
678
|
WHERE table_schema = $1
|
|
394
679
|
AND table_type = 'BASE TABLE'
|
|
395
680
|
ORDER BY table_name`,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
681
|
+
[schema],
|
|
682
|
+
),
|
|
683
|
+
// Query all columns for all tables in schema
|
|
684
|
+
driver.query<{
|
|
685
|
+
table_name: string;
|
|
686
|
+
column_name: string;
|
|
687
|
+
data_type: string;
|
|
688
|
+
udt_name: string;
|
|
689
|
+
is_nullable: string;
|
|
690
|
+
character_maximum_length: number | null;
|
|
691
|
+
numeric_precision: number | null;
|
|
692
|
+
numeric_scale: number | null;
|
|
693
|
+
column_default: string | null;
|
|
694
|
+
formatted_type: string | null;
|
|
695
|
+
}>(
|
|
696
|
+
`SELECT
|
|
412
697
|
c.table_name,
|
|
413
698
|
column_name,
|
|
414
699
|
data_type,
|
|
@@ -432,16 +717,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
432
717
|
AND NOT a.attisdropped
|
|
433
718
|
WHERE c.table_schema = $1
|
|
434
719
|
ORDER BY c.table_name, c.ordinal_position`,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
720
|
+
[schema],
|
|
721
|
+
),
|
|
722
|
+
// Query all primary keys for all tables in schema
|
|
723
|
+
driver.query<{
|
|
724
|
+
table_name: string;
|
|
725
|
+
constraint_name: string;
|
|
726
|
+
column_name: string;
|
|
727
|
+
ordinal_position: number;
|
|
728
|
+
}>(
|
|
729
|
+
`SELECT
|
|
445
730
|
tc.table_name,
|
|
446
731
|
tc.constraint_name,
|
|
447
732
|
kcu.column_name,
|
|
@@ -454,24 +739,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
454
739
|
WHERE tc.table_schema = $1
|
|
455
740
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
456
741
|
ORDER BY tc.table_name, kcu.ordinal_position`,
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
742
|
+
[schema],
|
|
743
|
+
),
|
|
744
|
+
// Query all foreign keys for all tables in schema, including referential actions.
|
|
745
|
+
// Uses pg_catalog for correct positional pairing of composite FK columns
|
|
746
|
+
// (information_schema.constraint_column_usage lacks ordinal_position,
|
|
747
|
+
// which causes Cartesian products for multi-column FKs).
|
|
748
|
+
driver.query<{
|
|
749
|
+
table_name: string;
|
|
750
|
+
constraint_name: string;
|
|
751
|
+
column_name: string;
|
|
752
|
+
ordinal_position: number;
|
|
753
|
+
referenced_table_schema: string;
|
|
754
|
+
referenced_table_name: string;
|
|
755
|
+
referenced_column_name: string;
|
|
756
|
+
delete_rule: string;
|
|
757
|
+
update_rule: string;
|
|
758
|
+
}>(
|
|
759
|
+
`SELECT
|
|
475
760
|
tc.table_name,
|
|
476
761
|
tc.constraint_name,
|
|
477
762
|
kcu.column_name,
|
|
@@ -504,16 +789,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
504
789
|
WHERE tc.table_schema = $1
|
|
505
790
|
AND tc.constraint_type = 'FOREIGN KEY'
|
|
506
791
|
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
792
|
+
[schema],
|
|
793
|
+
),
|
|
794
|
+
// Query all unique constraints for all tables in schema (excluding PKs)
|
|
795
|
+
driver.query<{
|
|
796
|
+
table_name: string;
|
|
797
|
+
constraint_name: string;
|
|
798
|
+
column_name: string;
|
|
799
|
+
ordinal_position: number;
|
|
800
|
+
}>(
|
|
801
|
+
`SELECT
|
|
517
802
|
tc.table_name,
|
|
518
803
|
tc.constraint_name,
|
|
519
804
|
kcu.column_name,
|
|
@@ -526,31 +811,31 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
526
811
|
WHERE tc.table_schema = $1
|
|
527
812
|
AND tc.constraint_type = 'UNIQUE'
|
|
528
813
|
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
814
|
+
[schema],
|
|
815
|
+
),
|
|
816
|
+
// Query all indexes for all tables in schema (excluding constraints).
|
|
817
|
+
// `index_position` is the column's position within the index (1-based),
|
|
818
|
+
// derived from `pg_index.indkey` so composite indexes round-trip with
|
|
819
|
+
// their declared column order intact.
|
|
820
|
+
driver.query<{
|
|
821
|
+
tablename: string;
|
|
822
|
+
indexname: string;
|
|
823
|
+
indisunique: boolean;
|
|
824
|
+
attname: string | null;
|
|
825
|
+
index_position: number;
|
|
826
|
+
amname: string | null;
|
|
827
|
+
reloptions: string[] | null;
|
|
828
|
+
}>(
|
|
829
|
+
// `ix.indkey` is an int2vector of column numbers in the order the
|
|
830
|
+
// columns appear in the index definition. Unnest it WITH ORDINALITY
|
|
831
|
+
// so each (index, column) row carries its position in the index,
|
|
832
|
+
// then ORDER BY that position. Without this the rows come back in
|
|
833
|
+
// table-column order (`a.attnum`), which silently shuffles the
|
|
834
|
+
// columns of any composite index whose index order differs from
|
|
835
|
+
// the table order — verification compares against the contract
|
|
836
|
+
// with order-sensitive equality and reports a spurious
|
|
837
|
+
// `index_mismatch`.
|
|
838
|
+
`SELECT
|
|
554
839
|
i.tablename,
|
|
555
840
|
i.indexname,
|
|
556
841
|
ix.indisunique,
|
|
@@ -576,9 +861,34 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
576
861
|
AND tc.constraint_name = i.indexname
|
|
577
862
|
)
|
|
578
863
|
ORDER BY i.tablename, i.indexname, k.ord`,
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
864
|
+
[schema],
|
|
865
|
+
),
|
|
866
|
+
// Query all check constraints for enum-restricted columns.
|
|
867
|
+
// `pg_get_constraintdef(oid)` returns the predicate including the
|
|
868
|
+
// `CHECK (...)` wrapper. We parse the inner predicate to extract
|
|
869
|
+
// the column name and permitted values.
|
|
870
|
+
//
|
|
871
|
+
// Scope: only parses the `= ANY (ARRAY[...])` and `IN (...)` shapes
|
|
872
|
+
// that this slice emits. Arbitrary SQL predicates are left as-is
|
|
873
|
+
// and will not produce check IR entries (they are silently skipped).
|
|
874
|
+
driver.query<{
|
|
875
|
+
table_name: string;
|
|
876
|
+
constraint_name: string;
|
|
877
|
+
constraintdef: string;
|
|
878
|
+
}>(
|
|
879
|
+
`SELECT
|
|
880
|
+
cl.relname AS table_name,
|
|
881
|
+
c.conname AS constraint_name,
|
|
882
|
+
pg_get_constraintdef(c.oid) AS constraintdef
|
|
883
|
+
FROM pg_catalog.pg_constraint c
|
|
884
|
+
JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
|
|
885
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
|
886
|
+
WHERE ns.nspname = $1
|
|
887
|
+
AND c.contype = 'c'
|
|
888
|
+
ORDER BY cl.relname, c.conname`,
|
|
889
|
+
[schema],
|
|
890
|
+
),
|
|
891
|
+
]);
|
|
582
892
|
|
|
583
893
|
// Group results by table name for efficient lookup
|
|
584
894
|
const columnsByTable = groupBy(columnsResult.rows, 'table_name');
|
|
@@ -586,6 +896,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
586
896
|
const fksByTable = groupBy(fkResult.rows, 'table_name');
|
|
587
897
|
const uniquesByTable = groupBy(uniqueResult.rows, 'table_name');
|
|
588
898
|
const indexesByTable = groupBy(indexResult.rows, 'tablename');
|
|
899
|
+
const checksByTable = groupBy(checkResult.rows, 'table_name');
|
|
589
900
|
|
|
590
901
|
// Get set of PK constraint names per table (to exclude from uniques)
|
|
591
902
|
const pkConstraintsByTable = new Map<string, Set<string>>();
|
|
@@ -757,6 +1068,21 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
757
1068
|
...(idx.options !== undefined && { options: idx.options }),
|
|
758
1069
|
}));
|
|
759
1070
|
|
|
1071
|
+
// Process check constraints — parse each predicate into column + value set.
|
|
1072
|
+
// Only the two shapes emitted by this slice are recognised; free-form
|
|
1073
|
+
// predicates are silently skipped (they won't produce check IR entries).
|
|
1074
|
+
const checksForTable: SqlCheckConstraintIRInput[] = [];
|
|
1075
|
+
for (const checkRow of checksByTable.get(tableName) ?? []) {
|
|
1076
|
+
const parsed = parseCheckConstraintDef(checkRow.constraintdef);
|
|
1077
|
+
if (parsed) {
|
|
1078
|
+
checksForTable.push({
|
|
1079
|
+
name: checkRow.constraint_name,
|
|
1080
|
+
column: parsed.column,
|
|
1081
|
+
permittedValues: parsed.permittedValues,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
760
1086
|
tables[tableName] = {
|
|
761
1087
|
name: tableName,
|
|
762
1088
|
columns,
|
|
@@ -764,6 +1090,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
764
1090
|
foreignKeys,
|
|
765
1091
|
uniques,
|
|
766
1092
|
indexes,
|
|
1093
|
+
...ifDefined('checks', checksForTable.length > 0 ? checksForTable : undefined),
|
|
767
1094
|
};
|
|
768
1095
|
}
|
|
769
1096
|
|
|
@@ -793,9 +1120,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
793
1120
|
/**
|
|
794
1121
|
* Gets the Postgres version from the database.
|
|
795
1122
|
*/
|
|
796
|
-
private async getPostgresVersion(
|
|
797
|
-
driver: ControlDriverInstance<'sql', 'postgres'>,
|
|
798
|
-
): Promise<string> {
|
|
1123
|
+
private async getPostgresVersion(driver: SqlControlDriverInstance<'postgres'>): Promise<string> {
|
|
799
1124
|
const result = await driver.query<{ version: string }>('SELECT version() AS version', []);
|
|
800
1125
|
const versionString = result.rows[0]?.version ?? '';
|
|
801
1126
|
// Extract version number from "PostgreSQL 15.1 ..." format
|
|
@@ -947,3 +1272,100 @@ function groupBy<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T
|
|
|
947
1272
|
}
|
|
948
1273
|
return map;
|
|
949
1274
|
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Parses a Postgres check-constraint definition string (as returned by
|
|
1278
|
+
* `pg_get_constraintdef`) into a column name and permitted values array.
|
|
1279
|
+
*
|
|
1280
|
+
* Handles two shapes that Postgres emits for enum-style checks:
|
|
1281
|
+
*
|
|
1282
|
+
* 1. `= ANY (ARRAY[...])` — Postgres rewrites `col IN ('a','b')` to this form:
|
|
1283
|
+
* `CHECK ((col = ANY (ARRAY['a'::text, 'b'::text])))`
|
|
1284
|
+
*
|
|
1285
|
+
* 2. `IN (...)` — stays as-is when written directly:
|
|
1286
|
+
* `CHECK ((col IN ('a', 'b')))`
|
|
1287
|
+
*
|
|
1288
|
+
* Column names may be plain identifiers (`status`) or double-quoted identifiers
|
|
1289
|
+
* (`"my-col"`). Double-quoted identifiers with embedded `""` are un-escaped to a
|
|
1290
|
+
* single `"`.
|
|
1291
|
+
*
|
|
1292
|
+
* String literal values may contain Postgres-style doubled single-quotes (`''`),
|
|
1293
|
+
* which are un-escaped to a single `'` (e.g. `O''Brien` → `O'Brien`).
|
|
1294
|
+
*
|
|
1295
|
+
* Returns `{ column, permittedValues }` when the predicate matches one of
|
|
1296
|
+
* the two recognised shapes. Returns `undefined` for anything else (e.g.
|
|
1297
|
+
* a free-form SQL predicate that wasn't emitted by this slice).
|
|
1298
|
+
*/
|
|
1299
|
+
export function parseCheckConstraintDef(
|
|
1300
|
+
constraintdef: string,
|
|
1301
|
+
): { column: string; permittedValues: readonly string[] } | undefined {
|
|
1302
|
+
// Strip outer `CHECK (...)` wrapper and any extra parentheses.
|
|
1303
|
+
// pg_get_constraintdef returns e.g. `CHECK ((col = ANY (ARRAY[...])))` — note
|
|
1304
|
+
// the double parens: one from CHECK and one that Postgres wraps the predicate
|
|
1305
|
+
// in. Strip both outer layers.
|
|
1306
|
+
const afterCheck = constraintdef
|
|
1307
|
+
.replace(/^CHECK\s*\(/i, '')
|
|
1308
|
+
.replace(/\)$/, '')
|
|
1309
|
+
.trim();
|
|
1310
|
+
// Strip one more optional paren pair (the inner wrap Postgres adds)
|
|
1311
|
+
const inner =
|
|
1312
|
+
afterCheck.startsWith('(') && afterCheck.endsWith(')')
|
|
1313
|
+
? afterCheck.slice(1, -1).trim()
|
|
1314
|
+
: afterCheck;
|
|
1315
|
+
|
|
1316
|
+
// Shape 1: col = ANY (ARRAY['a'::text, 'b'::text])
|
|
1317
|
+
// Accepts both plain identifiers and double-quoted identifiers for the column.
|
|
1318
|
+
// Anchored at the end so a composite predicate (e.g. `col = ANY (...) AND x > 0`)
|
|
1319
|
+
// does not partial-match.
|
|
1320
|
+
const anyArrayMatch = inner.match(
|
|
1321
|
+
/^(?:"((?:[^"]|"")*)"|(\w+))\s*=\s*ANY\s*\(\s*ARRAY\s*\[(.+)\]\s*\)\s*$/i,
|
|
1322
|
+
);
|
|
1323
|
+
if (anyArrayMatch) {
|
|
1324
|
+
const column =
|
|
1325
|
+
anyArrayMatch[1] !== undefined ? anyArrayMatch[1].replace(/""/g, '"') : anyArrayMatch[2];
|
|
1326
|
+
const arrayBody = anyArrayMatch[3];
|
|
1327
|
+
if (!column || !arrayBody) return undefined;
|
|
1328
|
+
const permittedValues = extractArrayLiterals(arrayBody);
|
|
1329
|
+
return permittedValues ? { column, permittedValues } : undefined;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Shape 2: col IN ('a', 'b')
|
|
1333
|
+
// Accepts both plain identifiers and double-quoted identifiers for the column.
|
|
1334
|
+
// Anchored at the end so a composite predicate (e.g. `col IN (...) AND x > 0`)
|
|
1335
|
+
// does not partial-match.
|
|
1336
|
+
const inMatch = inner.match(/^(?:"((?:[^"]|"")*)"|(\w+))\s+IN\s*\((.+)\)\s*$/i);
|
|
1337
|
+
if (inMatch) {
|
|
1338
|
+
const column = inMatch[1] !== undefined ? inMatch[1].replace(/""/g, '"') : inMatch[2];
|
|
1339
|
+
const listBody = inMatch[3];
|
|
1340
|
+
if (!column || !listBody) return undefined;
|
|
1341
|
+
const permittedValues = extractQuotedLiterals(listBody);
|
|
1342
|
+
return permittedValues ? { column, permittedValues } : undefined;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return undefined;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Extracts string literals from an `ARRAY[...]` body.
|
|
1350
|
+
* Handles `'value'::type` casts by stripping the cast part.
|
|
1351
|
+
* Postgres stores single quotes inside values as doubled single-quotes (`''`);
|
|
1352
|
+
* each extracted value is un-escaped so `O''Brien` becomes `O'Brien`.
|
|
1353
|
+
*/
|
|
1354
|
+
function extractArrayLiterals(arrayBody: string): readonly string[] | undefined {
|
|
1355
|
+
// Match 'value'::cast or 'value' (with possible spaces)
|
|
1356
|
+
const pattern = /'((?:[^'\\]|\\.|'')*)'\s*(?:::[^\s,\]]+)?/g;
|
|
1357
|
+
const values = [...arrayBody.matchAll(pattern)].map((m) => (m[1] ?? '').replace(/''/g, "'"));
|
|
1358
|
+
return values.length > 0 ? values : undefined;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Extracts string literals from an `IN (...)` list.
|
|
1363
|
+
* Handles single-quoted literals with possible escaped quotes.
|
|
1364
|
+
* Postgres stores single quotes inside values as doubled single-quotes (`''`);
|
|
1365
|
+
* each extracted value is un-escaped so `O''Brien` becomes `O'Brien`.
|
|
1366
|
+
*/
|
|
1367
|
+
function extractQuotedLiterals(listBody: string): readonly string[] | undefined {
|
|
1368
|
+
const pattern = /'((?:[^'\\]|\\.|'')*)'/g;
|
|
1369
|
+
const values = [...listBody.matchAll(pattern)].map((m) => (m[1] ?? '').replace(/''/g, "'"));
|
|
1370
|
+
return values.length > 0 ? values : undefined;
|
|
1371
|
+
}
|