@prisma-next/family-sql 0.5.0-dev.3 → 0.5.0-dev.31
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 +1024 -11
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/schema-verify.d.mts +2 -2
- package/dist/{types-C6K4mxDM.d.mts → types-sZihdnGx.d.mts} +14 -5
- package/dist/types-sZihdnGx.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 +18 -18
- package/src/core/control-adapter.ts +12 -0
- package/src/core/control-instance.ts +43 -15
- package/src/core/migrations/types.ts +7 -0
- 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/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/dist/verify.d.mts
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
import { ControlDriverInstance } from "@prisma-next/framework-components/control";
|
|
2
1
|
import { ContractMarkerRecord } from "@prisma-next/contract/types";
|
|
3
2
|
|
|
4
3
|
//#region src/core/verify.d.ts
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
declare function parseContractMarkerRow(row: unknown): ContractMarkerRecord;
|
|
11
|
-
/**
|
|
12
|
-
* Returns the SQL statement to read the contract marker.
|
|
13
|
-
* This is a migration-plane helper (no runtime imports).
|
|
14
|
-
* @internal - Used internally by readMarker(). Prefer readMarker() for Control Plane usage.
|
|
6
|
+
* Wire shape of a `prisma_contract.marker` row as it comes out of a SQL
|
|
7
|
+
* driver. Snake-cased to match the on-disk column names. Shared by every
|
|
8
|
+
* SQL target's `readMarker` so each runner doesn't redeclare it inline.
|
|
15
9
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
type ContractMarkerRow = {
|
|
11
|
+
core_hash: string;
|
|
12
|
+
profile_hash: string;
|
|
13
|
+
contract_json: unknown | null;
|
|
14
|
+
canonical_version: number | null;
|
|
15
|
+
updated_at: Date | string;
|
|
16
|
+
app_tag: string | null;
|
|
17
|
+
meta: unknown | null;
|
|
18
|
+
invariants: unknown;
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* This abstracts SQL-specific details from the Control Plane.
|
|
24
|
-
*
|
|
25
|
-
* @param driver - ControlDriverInstance instance for executing queries
|
|
26
|
-
* @returns Promise resolving to ContractMarkerRecord or null if marker not found
|
|
21
|
+
* Parses a contract marker row from database query result.
|
|
22
|
+
* This is SQL-specific parsing logic (handles SQL row structure with snake_case columns).
|
|
27
23
|
*/
|
|
28
|
-
declare function
|
|
24
|
+
declare function parseContractMarkerRow(row: unknown): ContractMarkerRecord;
|
|
29
25
|
//#endregion
|
|
30
|
-
export {
|
|
26
|
+
export { type ContractMarkerRow, parseContractMarkerRow };
|
|
31
27
|
//# sourceMappingURL=verify.d.mts.map
|
package/dist/verify.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.mts","names":[],"sources":["../src/core/verify.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify.d.mts","names":[],"sources":["../src/core/verify.ts"],"sourcesContent":[],"mappings":";;;;;;AAiDA;AA6BA;;KA7BY,iBAAA;;;;;cAKE;;;;;;;;;iBAwBE,sBAAA,gBAAsC"}
|
package/dist/verify.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as parseContractMarkerRow } from "./verify-BdES8wgQ.mjs";
|
|
2
2
|
|
|
3
|
-
export { parseContractMarkerRow
|
|
3
|
+
export { parseContractMarkerRow };
|
package/package.json
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/family-sql",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.31",
|
|
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.5.0-dev.
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/
|
|
12
|
-
"@prisma-next/
|
|
13
|
-
"@prisma-next/
|
|
14
|
-
"@prisma-next/
|
|
15
|
-
"@prisma-next/
|
|
16
|
-
"@prisma-next/sql-
|
|
17
|
-
"@prisma-next/sql-
|
|
18
|
-
"@prisma-next/
|
|
19
|
-
"@prisma-next/sql-
|
|
20
|
-
"@prisma-next/sql-
|
|
21
|
-
"@prisma-next/sql-
|
|
22
|
-
"@prisma-next/utils": "0.5.0-dev.3"
|
|
9
|
+
"@prisma-next/contract": "0.5.0-dev.31",
|
|
10
|
+
"@prisma-next/framework-components": "0.5.0-dev.31",
|
|
11
|
+
"@prisma-next/emitter": "0.5.0-dev.31",
|
|
12
|
+
"@prisma-next/sql-contract": "0.5.0-dev.31",
|
|
13
|
+
"@prisma-next/migration-tools": "0.5.0-dev.31",
|
|
14
|
+
"@prisma-next/operations": "0.5.0-dev.31",
|
|
15
|
+
"@prisma-next/sql-contract-emitter": "0.5.0-dev.31",
|
|
16
|
+
"@prisma-next/sql-operations": "0.5.0-dev.31",
|
|
17
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.31",
|
|
18
|
+
"@prisma-next/utils": "0.5.0-dev.31",
|
|
19
|
+
"@prisma-next/sql-contract-ts": "0.5.0-dev.31",
|
|
20
|
+
"@prisma-next/sql-schema-ir": "0.5.0-dev.31",
|
|
21
|
+
"@prisma-next/sql-runtime": "0.5.0-dev.31"
|
|
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/
|
|
29
|
-
"@prisma-next/
|
|
27
|
+
"@prisma-next/psl-parser": "0.5.0-dev.31",
|
|
28
|
+
"@prisma-next/driver-postgres": "0.5.0-dev.31",
|
|
29
|
+
"@prisma-next/sql-contract-psl": "0.5.0-dev.31",
|
|
30
|
+
"@prisma-next/psl-printer": "0.5.0-dev.31",
|
|
30
31
|
"@prisma-next/test-utils": "0.0.1",
|
|
31
|
-
"@prisma-next/psl-parser": "0.5.0-dev.3",
|
|
32
32
|
"@prisma-next/tsconfig": "0.0.0",
|
|
33
33
|
"@prisma-next/tsdown": "0.0.0"
|
|
34
34
|
},
|
|
@@ -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 {
|
|
@@ -296,6 +296,12 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
|
296
296
|
* All components must have matching familyId ('sql') and targetId.
|
|
297
297
|
*/
|
|
298
298
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
299
|
+
/**
|
|
300
|
+
* Invariant ids contributed by this apply (the migration's `providedInvariants`).
|
|
301
|
+
* The runner unions these into `marker.invariants` atomically with the marker write.
|
|
302
|
+
* Defaults to `[]` for marker-only flows (`db update`, `db init`).
|
|
303
|
+
*/
|
|
304
|
+
readonly invariants?: readonly string[];
|
|
299
305
|
}
|
|
300
306
|
|
|
301
307
|
export type SqlMigrationRunnerErrorCode =
|
|
@@ -305,6 +311,7 @@ export type SqlMigrationRunnerErrorCode =
|
|
|
305
311
|
| 'PRECHECK_FAILED'
|
|
306
312
|
| 'POSTCHECK_FAILED'
|
|
307
313
|
| 'SCHEMA_VERIFY_FAILED'
|
|
314
|
+
| 'FOREIGN_KEY_VIOLATION'
|
|
308
315
|
| 'EXECUTION_FAILED';
|
|
309
316
|
|
|
310
317
|
export interface SqlMigrationRunnerFailure extends MigrationRunnerFailure {
|
|
@@ -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
|
+
}
|