@prisma-next/family-sql 0.4.2 → 0.4.3
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 +2 -2
- package/dist/control-adapter.d.mts +12 -1
- package/dist/control-adapter.d.mts.map +1 -1
- package/dist/control.d.mts +3 -2
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1025 -11
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +14 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +16 -1
- package/dist/migration.mjs.map +1 -1
- package/dist/schema-verify.d.mts +2 -2
- package/dist/{types-C6K4mxDM.d.mts → types-gLyIyd2X.d.mts} +36 -11
- package/dist/types-gLyIyd2X.d.mts.map +1 -0
- package/dist/verify-BdES8wgQ.mjs +82 -0
- package/dist/verify-BdES8wgQ.mjs.map +1 -0
- package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +1 -1
- package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-_EoNcGIq.d.mts} +2 -2
- package/dist/{verify-sql-schema-BBhkqEDo.d.mts.map → verify-sql-schema-_EoNcGIq.d.mts.map} +1 -1
- package/dist/verify.d.mts +16 -20
- package/dist/verify.d.mts.map +1 -1
- package/dist/verify.mjs +2 -2
- package/package.json +20 -20
- package/src/core/control-adapter.ts +12 -0
- package/src/core/control-instance.ts +43 -15
- package/src/core/migrations/plan-helpers.ts +1 -0
- package/src/core/migrations/types.ts +29 -6
- package/src/core/operation-preview.ts +62 -0
- package/src/core/psl-contract-infer/default-mapping.ts +56 -0
- package/src/core/psl-contract-infer/name-transforms.ts +178 -0
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
- package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
- package/src/core/psl-contract-infer/printer-config.ts +55 -0
- package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
- package/src/core/psl-contract-infer/relation-inference.ts +196 -0
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
- package/src/core/sql-migration.ts +16 -1
- package/src/core/verify.ts +46 -108
- package/src/exports/verify.ts +1 -1
- package/dist/types-C6K4mxDM.d.mts.map +0 -1
- package/dist/verify-4GshvY4p.mjs +0 -122
- package/dist/verify-4GshvY4p.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/family-sql",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "SQL family descriptor for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.0.0",
|
|
9
|
-
"@prisma-next/contract": "0.4.
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/emitter": "0.4.
|
|
12
|
-
"@prisma-next/
|
|
13
|
-
"@prisma-next/
|
|
14
|
-
"@prisma-next/sql-contract": "0.4.
|
|
15
|
-
"@prisma-next/
|
|
16
|
-
"@prisma-next/sql-contract-ts": "0.4.
|
|
17
|
-
"@prisma-next/
|
|
18
|
-
"@prisma-next/sql-
|
|
19
|
-
"@prisma-next/sql-
|
|
20
|
-
"@prisma-next/sql-
|
|
21
|
-
"@prisma-next/utils": "0.4.
|
|
22
|
-
"@prisma-next/sql-schema-ir": "0.4.2"
|
|
9
|
+
"@prisma-next/contract": "0.4.3",
|
|
10
|
+
"@prisma-next/framework-components": "0.4.3",
|
|
11
|
+
"@prisma-next/emitter": "0.4.3",
|
|
12
|
+
"@prisma-next/migration-tools": "0.4.3",
|
|
13
|
+
"@prisma-next/operations": "0.4.3",
|
|
14
|
+
"@prisma-next/sql-contract": "0.4.3",
|
|
15
|
+
"@prisma-next/sql-contract-emitter": "0.4.3",
|
|
16
|
+
"@prisma-next/sql-contract-ts": "0.4.3",
|
|
17
|
+
"@prisma-next/sql-operations": "0.4.3",
|
|
18
|
+
"@prisma-next/sql-relational-core": "0.4.3",
|
|
19
|
+
"@prisma-next/sql-runtime": "0.4.3",
|
|
20
|
+
"@prisma-next/sql-schema-ir": "0.4.3",
|
|
21
|
+
"@prisma-next/utils": "0.4.3"
|
|
23
22
|
},
|
|
24
23
|
"devDependencies": {
|
|
25
24
|
"tsdown": "0.18.4",
|
|
26
25
|
"typescript": "5.9.3",
|
|
27
26
|
"vitest": "4.0.17",
|
|
28
|
-
"@prisma-next/psl-parser": "0.4.
|
|
29
|
-
"@prisma-next/driver-postgres": "0.4.
|
|
30
|
-
"@prisma-next/sql-contract-psl": "0.4.
|
|
27
|
+
"@prisma-next/psl-parser": "0.4.3",
|
|
28
|
+
"@prisma-next/driver-postgres": "0.4.3",
|
|
29
|
+
"@prisma-next/sql-contract-psl": "0.4.3",
|
|
30
|
+
"@prisma-next/test-utils": "0.0.1",
|
|
31
31
|
"@prisma-next/tsconfig": "0.0.0",
|
|
32
|
-
"@prisma-next/
|
|
33
|
-
"@prisma-next/
|
|
32
|
+
"@prisma-next/psl-printer": "0.4.3",
|
|
33
|
+
"@prisma-next/tsdown": "0.0.0"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
1
2
|
import type {
|
|
2
3
|
ControlAdapterInstance,
|
|
3
4
|
ControlDriverInstance,
|
|
@@ -19,6 +20,17 @@ import type { DefaultNormalizer, NativeTypeNormalizer } from './schema-verify/ve
|
|
|
19
20
|
*/
|
|
20
21
|
export interface SqlControlAdapter<TTarget extends string = string>
|
|
21
22
|
extends ControlAdapterInstance<'sql', TTarget> {
|
|
23
|
+
/**
|
|
24
|
+
* Reads the contract marker from the database, returning `null` if the marker
|
|
25
|
+
* table or its row is missing. Implementations are responsible for the
|
|
26
|
+
* dialect-specific existence probe (e.g. Postgres `information_schema.tables`
|
|
27
|
+
* vs SQLite `sqlite_master`) and parameter placeholders.
|
|
28
|
+
*
|
|
29
|
+
* @param driver - ControlDriverInstance for executing queries (target-specific)
|
|
30
|
+
* @returns Resolved marker record, or `null` if not yet stamped.
|
|
31
|
+
*/
|
|
32
|
+
readMarker(driver: ControlDriverInstance<'sql', TTarget>): Promise<ContractMarkerRecord | null>;
|
|
33
|
+
|
|
22
34
|
/**
|
|
23
35
|
* Introspects a database schema and returns a raw SqlSchemaIR.
|
|
24
36
|
*
|
|
@@ -9,7 +9,11 @@ import type {
|
|
|
9
9
|
ControlFamilyInstance,
|
|
10
10
|
ControlStack,
|
|
11
11
|
CoreSchemaView,
|
|
12
|
+
MigrationPlanOperation,
|
|
12
13
|
OperationContext,
|
|
14
|
+
OperationPreview,
|
|
15
|
+
OperationPreviewCapable,
|
|
16
|
+
PslContractInferCapable,
|
|
13
17
|
SchemaViewCapable,
|
|
14
18
|
SignDatabaseResult,
|
|
15
19
|
VerifyDatabaseResult,
|
|
@@ -22,6 +26,7 @@ import {
|
|
|
22
26
|
VERIFY_CODE_TARGET_MISMATCH,
|
|
23
27
|
} from '@prisma-next/framework-components/control';
|
|
24
28
|
import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
|
|
29
|
+
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
25
30
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
26
31
|
import { validateContract as sqlValidateContract } from '@prisma-next/sql-contract/validate';
|
|
27
32
|
import {
|
|
@@ -37,8 +42,10 @@ import type {
|
|
|
37
42
|
SqlControlAdapterDescriptor,
|
|
38
43
|
SqlControlExtensionDescriptor,
|
|
39
44
|
} from './migrations/types';
|
|
45
|
+
import { sqlOperationsToPreview } from './operation-preview';
|
|
46
|
+
import { sqlSchemaIrToPslAst } from './psl-contract-infer/sql-schema-ir-to-psl-ast';
|
|
40
47
|
import { verifySqlSchema } from './schema-verify/verify-sql-schema';
|
|
41
|
-
import { collectSupportedCodecTypeIds
|
|
48
|
+
import { collectSupportedCodecTypeIds } from './verify';
|
|
42
49
|
|
|
43
50
|
function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
|
|
44
51
|
const typeIds = new Set<string>();
|
|
@@ -182,6 +189,8 @@ export interface SchemaVerifyOptions {
|
|
|
182
189
|
export interface SqlControlFamilyInstance
|
|
183
190
|
extends ControlFamilyInstance<'sql', SqlSchemaIR>,
|
|
184
191
|
SchemaViewCapable<SqlSchemaIR>,
|
|
192
|
+
PslContractInferCapable<SqlSchemaIR>,
|
|
193
|
+
OperationPreviewCapable,
|
|
185
194
|
SqlFamilyInstanceState {
|
|
186
195
|
validateContract(contractJson: unknown): Contract;
|
|
187
196
|
|
|
@@ -206,6 +215,10 @@ export interface SqlControlFamilyInstance
|
|
|
206
215
|
readonly driver: ControlDriverInstance<'sql', string>;
|
|
207
216
|
readonly contract?: unknown;
|
|
208
217
|
}): Promise<SqlSchemaIR>;
|
|
218
|
+
|
|
219
|
+
inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
|
|
220
|
+
|
|
221
|
+
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview;
|
|
209
222
|
}
|
|
210
223
|
|
|
211
224
|
export type SqlFamilyInstance = SqlControlFamilyInstance;
|
|
@@ -217,7 +230,9 @@ function isSqlControlAdapter<TTargetId extends string>(
|
|
|
217
230
|
typeof value === 'object' &&
|
|
218
231
|
value !== null &&
|
|
219
232
|
'introspect' in value &&
|
|
220
|
-
typeof (value as { introspect: unknown }).introspect === 'function'
|
|
233
|
+
typeof (value as { introspect: unknown }).introspect === 'function' &&
|
|
234
|
+
'readMarker' in value &&
|
|
235
|
+
typeof (value as { readMarker: unknown }).readMarker === 'function'
|
|
221
236
|
);
|
|
222
237
|
}
|
|
223
238
|
|
|
@@ -293,6 +308,20 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
293
308
|
extensionPacks: extensions,
|
|
294
309
|
});
|
|
295
310
|
|
|
311
|
+
// Family-instance methods accept `ControlDriverInstance<'sql', string>` —
|
|
312
|
+
// the family API isn't generic on the target id. Letting `isSqlControlAdapter`
|
|
313
|
+
// default its type parameter narrows the adapter to `SqlControlAdapter<string>`,
|
|
314
|
+
// which matches the family-level driver type without any cast at call sites.
|
|
315
|
+
const getControlAdapter = () => {
|
|
316
|
+
const controlAdapter = adapter.create(stack);
|
|
317
|
+
if (!isSqlControlAdapter(controlAdapter)) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
'Adapter does not implement SqlControlAdapter (missing introspect or readMarker)',
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return controlAdapter;
|
|
323
|
+
};
|
|
324
|
+
|
|
296
325
|
return {
|
|
297
326
|
familyId: 'sql',
|
|
298
327
|
codecTypeImports,
|
|
@@ -326,7 +355,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
326
355
|
const contractProfileHash = contract.profileHash;
|
|
327
356
|
const contractTarget = contract.target;
|
|
328
357
|
|
|
329
|
-
const marker = await readMarker(driver);
|
|
358
|
+
const marker = await getControlAdapter().readMarker(driver);
|
|
330
359
|
|
|
331
360
|
let missingCodecs: readonly string[] | undefined;
|
|
332
361
|
let codecCoverageSkipped = false;
|
|
@@ -435,10 +464,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
435
464
|
|
|
436
465
|
const contract = sqlValidateContract<Contract<SqlStorage>>(contractInput, emptyCodecLookup);
|
|
437
466
|
|
|
438
|
-
const controlAdapter =
|
|
439
|
-
if (!isSqlControlAdapter(controlAdapter)) {
|
|
440
|
-
throw new Error('Adapter does not implement SqlControlAdapter.introspect()');
|
|
441
|
-
}
|
|
467
|
+
const controlAdapter = getControlAdapter();
|
|
442
468
|
const schemaIR = await controlAdapter.introspect(driver, contractInput);
|
|
443
469
|
|
|
444
470
|
return verifySqlSchema({
|
|
@@ -474,7 +500,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
474
500
|
await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
|
|
475
501
|
await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
|
|
476
502
|
|
|
477
|
-
const existingMarker = await readMarker(driver);
|
|
503
|
+
const existingMarker = await getControlAdapter().readMarker(driver);
|
|
478
504
|
|
|
479
505
|
let markerCreated = false;
|
|
480
506
|
let markerUpdated = false;
|
|
@@ -551,19 +577,21 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
551
577
|
async readMarker(options: {
|
|
552
578
|
readonly driver: ControlDriverInstance<'sql', string>;
|
|
553
579
|
}): Promise<ContractMarkerRecord | null> {
|
|
554
|
-
return readMarker(options.driver);
|
|
580
|
+
return getControlAdapter().readMarker(options.driver);
|
|
555
581
|
},
|
|
556
582
|
async introspect(options: {
|
|
557
583
|
readonly driver: ControlDriverInstance<'sql', string>;
|
|
558
584
|
readonly contract?: unknown;
|
|
559
585
|
}): Promise<SqlSchemaIR> {
|
|
560
|
-
|
|
586
|
+
return getControlAdapter().introspect(options.driver, options.contract);
|
|
587
|
+
},
|
|
561
588
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
589
|
+
inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst {
|
|
590
|
+
return sqlSchemaIrToPslAst(schemaIR);
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
|
|
594
|
+
return sqlOperationsToPreview(operations);
|
|
567
595
|
},
|
|
568
596
|
|
|
569
597
|
toSchemaView(schema: SqlSchemaIR): CoreSchemaView {
|
|
@@ -101,6 +101,7 @@ export function createMigrationPlan<TTargetDetails>(
|
|
|
101
101
|
: {}),
|
|
102
102
|
destination: Object.freeze({ ...options.destination }),
|
|
103
103
|
operations: freezeOperations(options.operations),
|
|
104
|
+
providedInvariants: Object.freeze([...options.providedInvariants]),
|
|
104
105
|
...(options.meta ? { meta: cloneRecord(options.meta) } : {}),
|
|
105
106
|
});
|
|
106
107
|
}
|
|
@@ -197,6 +197,15 @@ export interface SqlMigrationPlan<TTargetDetails> extends MigrationPlan {
|
|
|
197
197
|
*/
|
|
198
198
|
readonly destination: SqlMigrationPlanContractInfo;
|
|
199
199
|
readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
|
|
200
|
+
/**
|
|
201
|
+
* Sorted, deduplicated invariant ids declared by this plan's data-transform
|
|
202
|
+
* ops. Required at the SQL-family layer (the SQL runners consume this as
|
|
203
|
+
* the source of truth for marker writes and self-edge no-op checks); the
|
|
204
|
+
* framework-level {@link MigrationPlan.providedInvariants} stays optional
|
|
205
|
+
* because `db init` / `db update` plans don't have a corresponding
|
|
206
|
+
* migration manifest.
|
|
207
|
+
*/
|
|
208
|
+
readonly providedInvariants: readonly string[];
|
|
200
209
|
readonly meta?: AnyRecord;
|
|
201
210
|
}
|
|
202
211
|
|
|
@@ -243,14 +252,21 @@ export interface SqlMigrationPlannerPlanOptions {
|
|
|
243
252
|
readonly policy: MigrationOperationPolicy;
|
|
244
253
|
readonly schemaName?: string;
|
|
245
254
|
/**
|
|
246
|
-
* The "from" contract (state the planner assumes the database starts at)
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
255
|
+
* The "from" contract (state the planner assumes the database starts at),
|
|
256
|
+
* or `null` for reconciliation flows that have no prior contract.
|
|
257
|
+
*
|
|
258
|
+
* Required at every call site so the structural fact "I have a prior
|
|
259
|
+
* contract / I don't" is visible in the type. `migration plan` supplies
|
|
260
|
+
* the previous bundle's `metadata.toContract`; `db update` / `db init`
|
|
261
|
+
* reconcile against the live schema and pass `null`. Strategies that
|
|
262
|
+
* need from/to column-shape comparisons (unsafe type change, nullability
|
|
250
263
|
* tightening) use this to decide whether to emit `dataTransform`
|
|
251
|
-
* placeholders
|
|
264
|
+
* placeholders; they short-circuit when it is `null`.
|
|
265
|
+
*
|
|
266
|
+
* Planners also derive the "from" identity they stamp onto the produced
|
|
267
|
+
* plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
|
|
252
268
|
*/
|
|
253
|
-
readonly fromContract
|
|
269
|
+
readonly fromContract: Contract<SqlStorage> | null;
|
|
254
270
|
/**
|
|
255
271
|
* Active framework components participating in this composition.
|
|
256
272
|
* SQL targets can interpret this list to derive database dependencies.
|
|
@@ -305,6 +321,7 @@ export type SqlMigrationRunnerErrorCode =
|
|
|
305
321
|
| 'PRECHECK_FAILED'
|
|
306
322
|
| 'POSTCHECK_FAILED'
|
|
307
323
|
| 'SCHEMA_VERIFY_FAILED'
|
|
324
|
+
| 'FOREIGN_KEY_VIOLATION'
|
|
308
325
|
| 'EXECUTION_FAILED';
|
|
309
326
|
|
|
310
327
|
export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
|
|
@@ -337,5 +354,11 @@ export interface CreateSqlMigrationPlanOptions<TTargetDetails> {
|
|
|
337
354
|
readonly origin?: SqlMigrationPlanContractInfo | null;
|
|
338
355
|
readonly destination: SqlMigrationPlanContractInfo;
|
|
339
356
|
readonly operations: readonly SqlMigrationPlanOperation<TTargetDetails>[];
|
|
357
|
+
/**
|
|
358
|
+
* Sorted, deduplicated invariant ids for this plan; mirrors the required
|
|
359
|
+
* field on {@link SqlMigrationPlan}. Callers without a migration manifest
|
|
360
|
+
* (`db init`, `db update`, planner-built plans) pass `[]`.
|
|
361
|
+
*/
|
|
362
|
+
readonly providedInvariants: readonly string[];
|
|
340
363
|
readonly meta?: AnyRecord;
|
|
341
364
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MigrationPlanOperation,
|
|
3
|
+
OperationPreview,
|
|
4
|
+
} from '@prisma-next/framework-components/control';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shape of an SQL execute step on `SqlMigrationPlanOperation`. Used for runtime
|
|
8
|
+
* type narrowing without importing the concrete SQL type.
|
|
9
|
+
*/
|
|
10
|
+
interface SqlExecuteStep {
|
|
11
|
+
readonly sql: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isDdlStatement(sqlStatement: string): boolean {
|
|
15
|
+
const trimmed = sqlStatement.trim().toLowerCase();
|
|
16
|
+
return (
|
|
17
|
+
trimmed.startsWith('create ') || trimmed.startsWith('alter ') || trimmed.startsWith('drop ')
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasExecuteSteps(
|
|
22
|
+
operation: MigrationPlanOperation,
|
|
23
|
+
): operation is MigrationPlanOperation & { readonly execute: readonly SqlExecuteStep[] } {
|
|
24
|
+
const candidate = operation as unknown as Record<string, unknown>;
|
|
25
|
+
if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return candidate['execute'].every(
|
|
29
|
+
(step: unknown) => typeof step === 'object' && step !== null && 'sql' in step,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts a best-effort SQL DDL preview for CLI plan output.
|
|
35
|
+
* Presentation-only: never used to decide migration correctness.
|
|
36
|
+
*/
|
|
37
|
+
export function extractSqlDdl(operations: readonly MigrationPlanOperation[]): string[] {
|
|
38
|
+
const statements: string[] = [];
|
|
39
|
+
for (const operation of operations) {
|
|
40
|
+
if (!hasExecuteSteps(operation)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const step of operation.execute) {
|
|
44
|
+
if (typeof step.sql === 'string' && isDdlStatement(step.sql)) {
|
|
45
|
+
statements.push(step.sql.trim());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return statements;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wraps `extractSqlDdl` into the family-agnostic `OperationPreview` shape.
|
|
54
|
+
* Each statement carries `language: 'sql'`.
|
|
55
|
+
*/
|
|
56
|
+
export function sqlOperationsToPreview(
|
|
57
|
+
operations: readonly MigrationPlanOperation[],
|
|
58
|
+
): OperationPreview {
|
|
59
|
+
return {
|
|
60
|
+
statements: extractSqlDdl(operations).map((text) => ({ text, language: 'sql' })),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_FUNCTION_ATTRIBUTES: Readonly<Record<string, string>> = {
|
|
4
|
+
'autoincrement()': '@default(autoincrement())',
|
|
5
|
+
'now()': '@default(now())',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface DefaultMappingOptions {
|
|
9
|
+
readonly functionAttributes?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly fallbackFunctionAttribute?: ((expression: string) => string | undefined) | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type DefaultMappingResult = { readonly attribute: string } | { readonly comment: string };
|
|
14
|
+
|
|
15
|
+
export function mapDefault(
|
|
16
|
+
columnDefault: ColumnDefault,
|
|
17
|
+
options?: DefaultMappingOptions,
|
|
18
|
+
): DefaultMappingResult {
|
|
19
|
+
switch (columnDefault.kind) {
|
|
20
|
+
case 'literal':
|
|
21
|
+
return { attribute: `@default(${formatLiteralValue(columnDefault.value)})` };
|
|
22
|
+
case 'function': {
|
|
23
|
+
const attribute =
|
|
24
|
+
options?.functionAttributes?.[columnDefault.expression] ??
|
|
25
|
+
DEFAULT_FUNCTION_ATTRIBUTES[columnDefault.expression] ??
|
|
26
|
+
options?.fallbackFunctionAttribute?.(columnDefault.expression);
|
|
27
|
+
return attribute
|
|
28
|
+
? { attribute }
|
|
29
|
+
: { comment: `// Raw default: ${columnDefault.expression.replace(/[\r\n]+/g, ' ')}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatLiteralValue(value: unknown): string {
|
|
35
|
+
if (value === null) {
|
|
36
|
+
return 'null';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
switch (typeof value) {
|
|
40
|
+
case 'boolean':
|
|
41
|
+
case 'number':
|
|
42
|
+
return String(value);
|
|
43
|
+
case 'string':
|
|
44
|
+
return quoteString(value);
|
|
45
|
+
default:
|
|
46
|
+
return quoteString(JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function quoteString(str: string): string {
|
|
51
|
+
return `"${escapeString(str)}"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeString(str: string): string {
|
|
55
|
+
return JSON.stringify(str).slice(1, -1);
|
|
56
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const PSL_RESERVED_WORDS = new Set(['model', 'enum', 'types', 'type', 'generator', 'datasource']);
|
|
2
|
+
|
|
3
|
+
const IDENTIFIER_PART_PATTERN = /[A-Za-z0-9]+/g;
|
|
4
|
+
|
|
5
|
+
type NameResult = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly map?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function hasSeparators(input: string): boolean {
|
|
11
|
+
return /[^A-Za-z0-9]/.test(input);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractIdentifierParts(input: string): string[] {
|
|
15
|
+
return input.match(IDENTIFIER_PART_PATTERN) ?? [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createSyntheticIdentifier(input: string): string {
|
|
19
|
+
let hash = 2166136261;
|
|
20
|
+
|
|
21
|
+
for (const char of input) {
|
|
22
|
+
hash ^= char.codePointAt(0) ?? 0;
|
|
23
|
+
hash = Math.imul(hash, 16777619);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `x${(hash >>> 0).toString(16)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sanitizeIdentifierCharacters(input: string): string {
|
|
30
|
+
const sanitized = input.replace(/[^\w]/g, '');
|
|
31
|
+
return sanitized.length > 0 ? sanitized : createSyntheticIdentifier(input);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function capitalize(word: string): string {
|
|
35
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function snakeToPascalCase(input: string): string {
|
|
39
|
+
const parts = extractIdentifierParts(input);
|
|
40
|
+
if (parts.length === 0) {
|
|
41
|
+
return capitalize(sanitizeIdentifierCharacters(input));
|
|
42
|
+
}
|
|
43
|
+
return parts.map(capitalize).join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function snakeToCamelCase(input: string): string {
|
|
47
|
+
const parts = extractIdentifierParts(input);
|
|
48
|
+
if (parts.length === 0) {
|
|
49
|
+
return sanitizeIdentifierCharacters(input);
|
|
50
|
+
}
|
|
51
|
+
const [firstPart = input, ...rest] = parts;
|
|
52
|
+
return firstPart.charAt(0).toLowerCase() + firstPart.slice(1) + rest.map(capitalize).join('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function needsEscaping(name: string): boolean {
|
|
56
|
+
return PSL_RESERVED_WORDS.has(name.toLowerCase()) || /^\d/.test(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function escapeName(name: string): string {
|
|
60
|
+
return `_${name}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function escapeIfNeeded(name: string): string {
|
|
64
|
+
return needsEscaping(name) ? escapeName(name) : name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toModelName(tableName: string): NameResult {
|
|
68
|
+
let name: string;
|
|
69
|
+
|
|
70
|
+
if (hasSeparators(tableName)) {
|
|
71
|
+
name = snakeToPascalCase(tableName);
|
|
72
|
+
} else {
|
|
73
|
+
name = tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (needsEscaping(name)) {
|
|
77
|
+
const escaped = escapeName(name);
|
|
78
|
+
return { name: escaped, map: tableName };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (name !== tableName) {
|
|
82
|
+
return { name, map: tableName };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { name };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function toFieldName(columnName: string): NameResult {
|
|
89
|
+
let name: string;
|
|
90
|
+
|
|
91
|
+
if (hasSeparators(columnName)) {
|
|
92
|
+
name = snakeToCamelCase(columnName);
|
|
93
|
+
} else {
|
|
94
|
+
name = columnName.charAt(0).toLowerCase() + columnName.slice(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (needsEscaping(name)) {
|
|
98
|
+
const escaped = escapeName(name);
|
|
99
|
+
return { name: escaped, map: columnName };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (name !== columnName) {
|
|
103
|
+
return { name, map: columnName };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { name };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function toEnumName(pgTypeName: string): NameResult {
|
|
110
|
+
let name: string;
|
|
111
|
+
|
|
112
|
+
if (hasSeparators(pgTypeName)) {
|
|
113
|
+
name = snakeToPascalCase(pgTypeName);
|
|
114
|
+
} else {
|
|
115
|
+
name = pgTypeName.charAt(0).toUpperCase() + pgTypeName.slice(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (needsEscaping(name)) {
|
|
119
|
+
const escaped = escapeName(name);
|
|
120
|
+
return { name: escaped, map: pgTypeName };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (name !== pgTypeName) {
|
|
124
|
+
return { name, map: pgTypeName };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { name };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function pluralize(word: string): string {
|
|
131
|
+
if (
|
|
132
|
+
word.endsWith('s') ||
|
|
133
|
+
word.endsWith('x') ||
|
|
134
|
+
word.endsWith('z') ||
|
|
135
|
+
word.endsWith('ch') ||
|
|
136
|
+
word.endsWith('sh')
|
|
137
|
+
) {
|
|
138
|
+
return `${word}es`;
|
|
139
|
+
}
|
|
140
|
+
if (word.endsWith('y') && !/[aeiou]y$/i.test(word)) {
|
|
141
|
+
return `${word.slice(0, -1)}ies`;
|
|
142
|
+
}
|
|
143
|
+
return `${word}s`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function deriveRelationFieldName(
|
|
147
|
+
fkColumns: readonly string[],
|
|
148
|
+
referencedTableName: string,
|
|
149
|
+
): string {
|
|
150
|
+
if (fkColumns.length === 1) {
|
|
151
|
+
const [col = referencedTableName] = fkColumns;
|
|
152
|
+
const stripped = col.replace(/_id$/i, '').replace(/Id$/, '');
|
|
153
|
+
|
|
154
|
+
if (stripped.length > 0 && stripped !== col) {
|
|
155
|
+
return escapeIfNeeded(snakeToCamelCase(stripped));
|
|
156
|
+
}
|
|
157
|
+
return escapeIfNeeded(snakeToCamelCase(referencedTableName));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return escapeIfNeeded(snakeToCamelCase(referencedTableName));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function deriveBackRelationFieldName(childModelName: string, isOneToOne: boolean): string {
|
|
164
|
+
const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
|
|
165
|
+
return isOneToOne ? base : pluralize(base);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function toNamedTypeName(columnName: string): string {
|
|
169
|
+
let name: string;
|
|
170
|
+
|
|
171
|
+
if (hasSeparators(columnName)) {
|
|
172
|
+
name = snakeToPascalCase(columnName);
|
|
173
|
+
} else {
|
|
174
|
+
name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return escapeIfNeeded(name);
|
|
178
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DefaultMappingOptions } from './default-mapping';
|
|
2
|
+
|
|
3
|
+
const POSTGRES_FUNCTION_ATTRIBUTES: Readonly<Record<string, string>> = {
|
|
4
|
+
'gen_random_uuid()': '@default(dbgenerated("gen_random_uuid()"))',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function formatDbGeneratedAttribute(expression: string): string {
|
|
8
|
+
return `@default(dbgenerated(${JSON.stringify(expression)}))`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createPostgresDefaultMapping(): DefaultMappingOptions {
|
|
12
|
+
return {
|
|
13
|
+
functionAttributes: POSTGRES_FUNCTION_ATTRIBUTES,
|
|
14
|
+
fallbackFunctionAttribute: formatDbGeneratedAttribute,
|
|
15
|
+
};
|
|
16
|
+
}
|