@prisma-next/target-sqlite 0.14.0-dev.6 → 0.14.0-dev.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract-free.mjs +1 -1
- package/dist/control.d.mts +3 -4
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +16 -27
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-DxmEeTJ-.mjs → descriptor-meta-D1fap-5t.mjs} +4 -11
- package/dist/descriptor-meta-D1fap-5t.mjs.map +1 -0
- package/dist/descriptor-meta-runtime-BkXK3OjD.mjs +12 -0
- package/dist/descriptor-meta-runtime-BkXK3OjD.mjs.map +1 -0
- package/dist/migration.d.mts +2 -2
- package/dist/migration.mjs +2 -2
- package/dist/native-type-normalizer.mjs +13 -1
- package/dist/native-type-normalizer.mjs.map +1 -0
- package/dist/{op-factory-call-DmdfD1yd.mjs → op-factory-call-Do03QgD9.mjs} +134 -117
- package/dist/op-factory-call-Do03QgD9.mjs.map +1 -0
- package/dist/op-factory-call.d.mts +2 -2
- package/dist/op-factory-call.d.mts.map +1 -1
- package/dist/op-factory-call.mjs +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/planner-DGhzXuGh.mjs +848 -0
- package/dist/planner-DGhzXuGh.mjs.map +1 -0
- package/dist/{planner-produced-sqlite-migration-0xPEm3R1.mjs → planner-produced-sqlite-migration-BDzhFAoW.mjs} +46 -17
- package/dist/planner-produced-sqlite-migration-BDzhFAoW.mjs.map +1 -0
- package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts → planner-produced-sqlite-migration-o1qUzwLk.d.mts} +3 -3
- package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts.map → planner-produced-sqlite-migration-o1qUzwLk.d.mts.map} +1 -1
- package/dist/planner-produced-sqlite-migration.d.mts +1 -1
- package/dist/planner-produced-sqlite-migration.mjs +1 -1
- package/dist/planner.d.mts +47 -11
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +1 -1
- package/dist/runtime.d.mts +6 -4
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +3 -3
- package/dist/{shared-Dhc8mLK1.d.mts → shared-C6sM-wel.d.mts} +2 -2
- package/dist/{shared-Dhc8mLK1.d.mts.map → shared-C6sM-wel.d.mts.map} +1 -1
- package/dist/{sqlite-contract-serializer--iaDgC8e.mjs → sqlite-contract-view-BEwi0mUC.mjs} +34 -9
- package/dist/sqlite-contract-view-BEwi0mUC.mjs.map +1 -0
- package/dist/sqlite-contract-view-J32K82Gi.d.mts +29 -0
- package/dist/sqlite-contract-view-J32K82Gi.d.mts.map +1 -0
- package/dist/sqlite-migration-BNiSiP0z.mjs +123 -0
- package/dist/sqlite-migration-BNiSiP0z.mjs.map +1 -0
- package/dist/{sqlite-migration-DVfhQwN_.d.mts → sqlite-migration-BYbzskpE.d.mts} +35 -6
- package/dist/sqlite-migration-BYbzskpE.d.mts.map +1 -0
- package/package.json +18 -18
- package/src/core/control-target.ts +13 -18
- package/src/core/errors.ts +14 -12
- package/src/core/migrations/column-ddl-rendering.ts +177 -0
- package/src/core/migrations/diff-database-schema.ts +230 -0
- package/src/core/migrations/issue-planner.ts +361 -488
- package/src/core/migrations/op-factory-call.ts +5 -0
- package/src/core/migrations/operations/tables.ts +107 -68
- package/src/core/migrations/planner-strategies.ts +118 -137
- package/src/core/migrations/planner.ts +117 -30
- package/src/core/migrations/render-typescript.ts +42 -15
- package/src/core/migrations/runner.ts +4 -10
- package/src/core/migrations/sqlite-migration.ts +69 -31
- package/src/core/sqlite-contract-serializer.ts +29 -14
- package/src/core/sqlite-contract-view.ts +41 -0
- package/src/core/sqlite-schema-verifier.ts +6 -3
- package/src/core/sqlite-unbound-database.ts +8 -8
- package/src/exports/runtime.ts +1 -0
- package/dist/descriptor-meta-DxmEeTJ-.mjs.map +0 -1
- package/dist/native-type-normalizer-CiSyVmMP.mjs +0 -14
- package/dist/native-type-normalizer-CiSyVmMP.mjs.map +0 -1
- package/dist/op-factory-call-DmdfD1yd.mjs.map +0 -1
- package/dist/planner-Ciq8p_dL.mjs +0 -622
- package/dist/planner-Ciq8p_dL.mjs.map +0 -1
- package/dist/planner-produced-sqlite-migration-0xPEm3R1.mjs.map +0 -1
- package/dist/sqlite-contract-serializer--iaDgC8e.mjs.map +0 -1
- package/dist/sqlite-migration-A0rwqPOG.mjs +0 -92
- package/dist/sqlite-migration-A0rwqPOG.mjs.map +0 -1
- package/dist/sqlite-migration-DVfhQwN_.d.mts.map +0 -1
|
@@ -11,16 +11,17 @@ import {
|
|
|
11
11
|
plannerFailure,
|
|
12
12
|
} from '@prisma-next/family-sql/control';
|
|
13
13
|
import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
|
|
14
|
-
import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
|
|
15
14
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
16
15
|
import type {
|
|
17
16
|
MigrationPlanner,
|
|
18
17
|
MigrationScaffoldContext,
|
|
19
|
-
|
|
18
|
+
SchemaDiffIssue,
|
|
19
|
+
SchemaOwnership,
|
|
20
20
|
} from '@prisma-next/framework-components/control';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
21
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
22
|
+
import { RelationalSchemaNodeKind, type SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
23
|
+
import { buildSqlitePlanDiff } from './diff-database-schema';
|
|
24
|
+
import { coalesceSubtreeIssues, issueNode, planIssues } from './issue-planner';
|
|
24
25
|
import {
|
|
25
26
|
type SqliteMigrationDestinationInfo,
|
|
26
27
|
TypeScriptRenderableSqliteMigration,
|
|
@@ -41,16 +42,18 @@ export type SqlitePlanResult =
|
|
|
41
42
|
/**
|
|
42
43
|
* SQLite migration planner — a thin wrapper over `planIssues`.
|
|
43
44
|
*
|
|
44
|
-
* `plan()`
|
|
45
|
-
* `
|
|
46
|
-
*
|
|
47
|
-
* recipes (e.g. recreating a
|
|
48
|
-
* constraint changes at once);
|
|
49
|
-
*
|
|
45
|
+
* `plan()` diffs the live schema against the target contract via the one
|
|
46
|
+
* differ (`buildSqlitePlanDiff`, producing node-typed `SchemaDiffIssue[]`)
|
|
47
|
+
* and delegates to `planIssues` with the registered strategies. Strategies
|
|
48
|
+
* absorb groups of related issues into composite recipes (e.g. recreating a
|
|
49
|
+
* table to apply type/nullability/default/constraint changes at once);
|
|
50
|
+
* anything not absorbed by a strategy flows through `mapNodeIssueToCall` in
|
|
51
|
+
* the issue planner as a one-off call.
|
|
50
52
|
*
|
|
51
|
-
* FK-backing indexes are
|
|
52
|
-
* (
|
|
53
|
-
* uniformly alongside user-declared
|
|
53
|
+
* FK-backing indexes are already merged into the expected table node's
|
|
54
|
+
* `indexes` at derivation (`contractToSchemaIR`'s `convertTable`), so
|
|
55
|
+
* `mapNodeIssueToCall` handles them uniformly alongside user-declared
|
|
56
|
+
* indexes — no separate expansion step in the planner.
|
|
54
57
|
*/
|
|
55
58
|
export class SqliteMigrationPlanner
|
|
56
59
|
implements SqlMigrationPlanner<SqlitePlanTargetDetails>, MigrationPlanner<'sql', 'sqlite'>
|
|
@@ -83,6 +86,11 @@ export class SqliteMigrationPlanner
|
|
|
83
86
|
* the marker row by the right space.
|
|
84
87
|
*/
|
|
85
88
|
readonly spaceId: string;
|
|
89
|
+
/**
|
|
90
|
+
* Ownership oracle over the contract-space composition — see
|
|
91
|
+
* {@link SqlMigrationPlannerPlanOptions.ownership}.
|
|
92
|
+
*/
|
|
93
|
+
readonly ownership?: SchemaOwnership;
|
|
86
94
|
}): SqlitePlanResult {
|
|
87
95
|
return this.planSql(options as SqlMigrationPlannerPlanOptions);
|
|
88
96
|
}
|
|
@@ -107,17 +115,13 @@ export class SqliteMigrationPlanner
|
|
|
107
115
|
const policyResult = this.ensureAdditivePolicy(options.policy);
|
|
108
116
|
if (policyResult) return policyResult;
|
|
109
117
|
|
|
110
|
-
const
|
|
118
|
+
const { expected, actual, issues } = this.collectSchemaIssues(options);
|
|
111
119
|
const codecHooks = extractCodecControlHooks(options.frameworkComponents);
|
|
112
|
-
const storageTypes = options.contract.storage.types ?? {};
|
|
113
120
|
|
|
114
121
|
const result = planIssues({
|
|
115
|
-
issues
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
codecHooks,
|
|
119
|
-
storageTypes,
|
|
120
|
-
schema: options.schema,
|
|
122
|
+
issues,
|
|
123
|
+
expected,
|
|
124
|
+
actual,
|
|
121
125
|
policy: options.policy,
|
|
122
126
|
frameworkComponents: options.frameworkComponents,
|
|
123
127
|
strategies: sqlitePlannerStrategies,
|
|
@@ -178,18 +182,101 @@ export class SqliteMigrationPlanner
|
|
|
178
182
|
return null;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Diffs the target contract against the live schema via the one differ
|
|
187
|
+
* (the same tree-building `diffSqliteSchema` uses, plus the
|
|
188
|
+
* op-render stamper) and prepares the issue list `planIssues` consumes.
|
|
189
|
+
*
|
|
190
|
+
* Three passes, in order:
|
|
191
|
+
* 1. Subtree coalescing — the differ is total (a missing/extra table also
|
|
192
|
+
* emits an issue for every column/constraint under it); those nested
|
|
193
|
+
* issues are redundant once the table-level `CreateTable`/`DropTable`
|
|
194
|
+
* call already accounts for the whole subtree. Runs FIRST, over the
|
|
195
|
+
* complete diff: a sibling-owned extra table's column issues must
|
|
196
|
+
* collapse into its one table-level issue before the ownership pass,
|
|
197
|
+
* because a bare column node carries no table reference — if coalescing
|
|
198
|
+
* ran after ownership dropped the table-level issue, the orphaned column
|
|
199
|
+
* issues would survive and the planner would emit drops against a
|
|
200
|
+
* sibling space's table.
|
|
201
|
+
* 2. Ownership — a live extra is only this plan's to drop when no contract
|
|
202
|
+
* space owns it. The differ ran against THIS space's contract, so a
|
|
203
|
+
* table a sibling owns surfaces here as `not-expected`; the planner asks
|
|
204
|
+
* the ownership oracle (the passive aggregate) whether any space declares
|
|
205
|
+
* it and, if so, leaves it alone. A table no space owns stays a genuine
|
|
206
|
+
* extra. Ownership lives in the aggregate; the planner only asks. No
|
|
207
|
+
* oracle (single-space) keeps every extra.
|
|
208
|
+
* 3. Strict-mode extras gating — `not-expected` (extra table/column/
|
|
209
|
+
* constraint) issues are dropped entirely outside strict mode, mirroring
|
|
210
|
+
* the retired coordinate walk's `if (strict) { ...extra_* } }` guards:
|
|
211
|
+
* an additive-only plan must never even consider dropping an unclaimed
|
|
212
|
+
* object, not just refuse to emit the drop.
|
|
213
|
+
*/
|
|
214
|
+
private collectSchemaIssues(options: SqlMigrationPlannerPlanOptions): {
|
|
215
|
+
readonly expected: SqlSchemaIR;
|
|
216
|
+
readonly actual: SqlSchemaIR;
|
|
217
|
+
readonly issues: readonly SchemaDiffIssue[];
|
|
218
|
+
} {
|
|
182
219
|
const allowed = options.policy.allowedOperationClasses;
|
|
183
220
|
const strict = allowed.includes('widening') || allowed.includes('destructive');
|
|
184
|
-
const
|
|
221
|
+
const {
|
|
222
|
+
expected,
|
|
223
|
+
actual,
|
|
224
|
+
issues: rawIssues,
|
|
225
|
+
} = buildSqlitePlanDiff({
|
|
185
226
|
contract: options.contract,
|
|
186
|
-
|
|
187
|
-
strict,
|
|
188
|
-
typeMetadataRegistry: new Map(),
|
|
227
|
+
actualSchema: options.schema,
|
|
189
228
|
frameworkComponents: options.frameworkComponents,
|
|
190
|
-
normalizeDefault: parseSqliteDefault,
|
|
191
|
-
normalizeNativeType: normalizeSqliteNativeType,
|
|
192
229
|
});
|
|
193
|
-
|
|
230
|
+
const coalesced = coalesceSubtreeIssues(rawIssues);
|
|
231
|
+
const owned = retainUnownedExtras(coalesced, options.ownership);
|
|
232
|
+
const issues = strict ? owned : owned.filter((issue) => issue.reason !== 'not-expected');
|
|
233
|
+
return { expected, actual, issues };
|
|
194
234
|
}
|
|
195
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Drops a `not-expected` issue when it is a whole extra TABLE that some
|
|
239
|
+
* contract space owns, asking the ownership oracle per node.
|
|
240
|
+
*
|
|
241
|
+
* The consultation applies ONLY to table-level extras — a live table this
|
|
242
|
+
* space's contract lacks — identified by asking the issue's own node
|
|
243
|
+
* (`nodeKind === table`), never by counting path segments. SQLite is a
|
|
244
|
+
* single-namespace target, so every coordinate is qualified with the shared
|
|
245
|
+
* `UNBOUND_NAMESPACE_ID` sentinel (the same one the aggregate's declared
|
|
246
|
+
* entities carry for this target). `entityKind` is the literal `'table'`:
|
|
247
|
+
* this function only ever asks about a node already confirmed to be
|
|
248
|
+
* `RelationalSchemaNodeKind.table` (checked just above), and that's a
|
|
249
|
+
* diff-tree `nodeKind` spelling (`'sql-table'`) distinct from the storage
|
|
250
|
+
* `entries` vocabulary `elementCoordinates` walks (`'table'`) — the literal
|
|
251
|
+
* is that storage-entries spelling. `declaresEntity` answers over the whole
|
|
252
|
+
* composition (self included), so a positive answer on such a table means
|
|
253
|
+
* another space owns it (a table this space owned would be in its expected
|
|
254
|
+
* tree, never an extra): leave it. A negative answer means no space owns
|
|
255
|
+
* it — a genuine orphan to drop.
|
|
256
|
+
*
|
|
257
|
+
* A DEEPER extra (an extra column/constraint on a table this space DOES own —
|
|
258
|
+
* only the child drifted, so the table is in the expected tree) is this
|
|
259
|
+
* space's own drift and is always kept for dropping; asking the oracle there
|
|
260
|
+
* would wrongly suppress it, because the owned table answers `true`. No oracle
|
|
261
|
+
* ⇒ every extra is kept (single-space plan).
|
|
262
|
+
*/
|
|
263
|
+
function retainUnownedExtras(
|
|
264
|
+
issues: readonly SchemaDiffIssue[],
|
|
265
|
+
ownership: SchemaOwnership | undefined,
|
|
266
|
+
): readonly SchemaDiffIssue[] {
|
|
267
|
+
if (ownership === undefined) return issues;
|
|
268
|
+
return issues.filter((issue) => {
|
|
269
|
+
if (issue.reason !== 'not-expected') return true;
|
|
270
|
+
const node = issueNode(issue);
|
|
271
|
+
if (node === undefined || node.nodeKind !== RelationalSchemaNodeKind.table) return true;
|
|
272
|
+
const tableName = issue.path[1];
|
|
273
|
+
return (
|
|
274
|
+
tableName === undefined ||
|
|
275
|
+
!ownership.declaresEntity({
|
|
276
|
+
namespaceId: UNBOUND_NAMESPACE_ID,
|
|
277
|
+
entityKind: 'table',
|
|
278
|
+
entityName: tableName,
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
@@ -35,15 +35,19 @@ export function renderCallsToTypeScript(
|
|
|
35
35
|
calls: ReadonlyArray<OpFactoryCall>,
|
|
36
36
|
meta: RenderMigrationMeta,
|
|
37
37
|
): string {
|
|
38
|
-
const imports = buildImports(calls);
|
|
38
|
+
const imports = buildImports(calls, meta);
|
|
39
39
|
const operationsBody = calls.map((c) => c.renderTypeScript()).join(',\n');
|
|
40
|
+
const hasStart = meta.from !== null;
|
|
41
|
+
const startField = hasStart ? [' override readonly startContractJson = startContract;'] : [];
|
|
40
42
|
|
|
41
43
|
return [
|
|
42
44
|
shebangLineFor(detectScaffoldRuntime()),
|
|
43
45
|
imports,
|
|
44
46
|
'',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
`export default class M extends Migration<${hasStart ? 'Start' : 'never'}, End> {`,
|
|
48
|
+
...startField,
|
|
49
|
+
' override readonly endContractJson = endContract;',
|
|
50
|
+
'',
|
|
47
51
|
' override get operations() {',
|
|
48
52
|
' return [',
|
|
49
53
|
indent(operationsBody, 6),
|
|
@@ -56,8 +60,8 @@ export function renderCallsToTypeScript(
|
|
|
56
60
|
].join('\n');
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
function buildImports(calls: ReadonlyArray<OpFactoryCall
|
|
60
|
-
const requirements: ImportRequirement[] = [...BASE_IMPORTS];
|
|
63
|
+
function buildImports(calls: ReadonlyArray<OpFactoryCall>, meta: RenderMigrationMeta): string {
|
|
64
|
+
const requirements: ImportRequirement[] = [...BASE_IMPORTS, ...contractImports(meta)];
|
|
61
65
|
for (const call of calls) {
|
|
62
66
|
for (const req of call.importRequirements()) {
|
|
63
67
|
requirements.push(req);
|
|
@@ -66,16 +70,39 @@ function buildImports(calls: ReadonlyArray<OpFactoryCall>): string {
|
|
|
66
70
|
return renderImports(requirements);
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The committed contract-JSON imports the scaffold reads its from/to identity
|
|
75
|
+
* from. `end-contract.json` is always present; `start-contract.json` is added
|
|
76
|
+
* only for a non-baseline migration (`meta.from !== null`). The matching
|
|
77
|
+
* `Contract` type imports (aliased `Start`/`End`) feed the
|
|
78
|
+
* `Migration<Start, End>` generics. Baseline emits `Migration<never, End>` with
|
|
79
|
+
* no start imports — `never` is the honest "no prior contract" Start.
|
|
80
|
+
*/
|
|
81
|
+
function contractImports(meta: RenderMigrationMeta): readonly ImportRequirement[] {
|
|
82
|
+
const reqs: ImportRequirement[] = [
|
|
83
|
+
{
|
|
84
|
+
moduleSpecifier: './end-contract.json',
|
|
85
|
+
symbol: 'endContract',
|
|
86
|
+
kind: 'default',
|
|
87
|
+
attributes: { type: 'json' },
|
|
88
|
+
},
|
|
89
|
+
{ moduleSpecifier: './end-contract', symbol: 'Contract', alias: 'End', typeOnly: true },
|
|
90
|
+
];
|
|
91
|
+
if (meta.from !== null) {
|
|
92
|
+
reqs.push({
|
|
93
|
+
moduleSpecifier: './start-contract.json',
|
|
94
|
+
symbol: 'startContract',
|
|
95
|
+
kind: 'default',
|
|
96
|
+
attributes: { type: 'json' },
|
|
97
|
+
});
|
|
98
|
+
reqs.push({
|
|
99
|
+
moduleSpecifier: './start-contract',
|
|
100
|
+
symbol: 'Contract',
|
|
101
|
+
alias: 'Start',
|
|
102
|
+
typeOnly: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return reqs;
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
function indent(text: string, spaces: number): string {
|
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
SqlMigrationRunnerSuccessValue,
|
|
13
13
|
} from '@prisma-next/family-sql/control';
|
|
14
14
|
import { runnerFailure, runnerSuccess } from '@prisma-next/family-sql/control';
|
|
15
|
-
import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
|
|
16
15
|
import type { MigrationRunnerResult } from '@prisma-next/framework-components/control';
|
|
17
16
|
import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
|
|
18
17
|
import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
@@ -22,8 +21,7 @@ import { ifDefined } from '@prisma-next/utils/defined';
|
|
|
22
21
|
import type { Result } from '@prisma-next/utils/result';
|
|
23
22
|
import { notOk, ok, okVoid } from '@prisma-next/utils/result';
|
|
24
23
|
import { MARKER_TABLE_NAME } from '../control-tables';
|
|
25
|
-
import {
|
|
26
|
-
import { normalizeSqliteNativeType } from '../native-type-normalizer';
|
|
24
|
+
import { verifySqliteDatabaseSchema } from './diff-database-schema';
|
|
27
25
|
import type { SqlitePlanTargetDetails } from './planner-target-details';
|
|
28
26
|
|
|
29
27
|
export function createSqliteMigrationRunner(
|
|
@@ -92,20 +90,16 @@ class SqliteMigrationRunner implements SqlMigrationRunner<SqlitePlanTargetDetail
|
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
if (space === APP_SPACE_ID) {
|
|
95
|
-
const
|
|
93
|
+
const schemaNode = await this.family.introspect({
|
|
96
94
|
driver,
|
|
97
95
|
contract: options.destinationContract,
|
|
98
96
|
});
|
|
99
|
-
|
|
100
|
-
const schemaVerifyResult = verifySqlSchema({
|
|
97
|
+
const schemaVerifyResult = verifySqliteDatabaseSchema({
|
|
101
98
|
contract: options.destinationContract,
|
|
102
|
-
|
|
99
|
+
actualSchema: schemaNode,
|
|
103
100
|
strict: options.strictVerification ?? true,
|
|
104
|
-
context: options.context ?? {},
|
|
105
101
|
typeMetadataRegistry: this.family.typeMetadataRegistry,
|
|
106
102
|
frameworkComponents: options.frameworkComponents,
|
|
107
|
-
normalizeDefault: parseSqliteDefault,
|
|
108
|
-
normalizeNativeType: normalizeSqliteNativeType,
|
|
109
103
|
});
|
|
110
104
|
if (!schemaVerifyResult.ok) {
|
|
111
105
|
return runnerFailure('SCHEMA_VERIFY_FAILED', schemaVerifyResult.summary, {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
1
2
|
import type {
|
|
2
3
|
MigrationOperationClass,
|
|
3
4
|
SqlMigrationPlanOperation,
|
|
@@ -5,9 +6,12 @@ import type {
|
|
|
5
6
|
import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
|
|
6
7
|
import { Migration as SqlMigration } from '@prisma-next/family-sql/migration';
|
|
7
8
|
import type { ControlStack } from '@prisma-next/framework-components/control';
|
|
9
|
+
import { MigrationContractViews } from '@prisma-next/migration-tools/migration';
|
|
10
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
8
11
|
import type { DdlColumn, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast';
|
|
9
12
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
10
13
|
import { errorSqliteMigrationStackMissing } from '../errors';
|
|
14
|
+
import { SqliteContractView } from '../sqlite-contract-view';
|
|
11
15
|
import {
|
|
12
16
|
AddColumnCall,
|
|
13
17
|
CreateIndexCall,
|
|
@@ -34,18 +38,38 @@ type Op = SqlMigrationPlanOperation<SqlitePlanTargetDetails>;
|
|
|
34
38
|
* forward to the corresponding `*Call` with that stored adapter, so user
|
|
35
39
|
* migrations can write `this.createTable({...})` without threading the adapter
|
|
36
40
|
* through every call.
|
|
41
|
+
*
|
|
42
|
+
* Binds the framework base's `Start` / `End` contract generics so a subclass
|
|
43
|
+
* that assigns its `start-contract.json` / `end-contract.json` imports gets
|
|
44
|
+
* fully-typed view accessors: `this.endContract` is a `SqliteContractView<End>`
|
|
45
|
+
* (sole namespace unwrapped to the root — `this.endContract.table.<name>`),
|
|
46
|
+
* built lazily from the JSON fields via the shared `MigrationContractViews`
|
|
47
|
+
* helper. Mirrors `MongoMigration`'s view getters; the framework base derives
|
|
48
|
+
* `describe()` from the same JSON.
|
|
37
49
|
*/
|
|
38
|
-
export abstract class SqliteMigration
|
|
50
|
+
export abstract class SqliteMigration<
|
|
51
|
+
Start extends Contract<SqlStorage> = Contract<SqlStorage>,
|
|
52
|
+
End extends Contract<SqlStorage> = Contract<SqlStorage>,
|
|
53
|
+
> extends SqlMigration<SqlitePlanTargetDetails, 'sqlite', Start, End> {
|
|
39
54
|
readonly targetId = 'sqlite' as const;
|
|
40
55
|
|
|
41
56
|
/**
|
|
42
57
|
* Materialized SQLite control adapter, created once per migration
|
|
43
58
|
* instance from the injected stack. `undefined` only when the migration
|
|
44
|
-
* was instantiated without a stack (test fixtures);
|
|
45
|
-
*
|
|
59
|
+
* was instantiated without a stack (test fixtures); `controlAdapterFor`
|
|
60
|
+
* throws a PN-MIG-2008 in that case to surface the misuse.
|
|
46
61
|
*/
|
|
47
62
|
protected readonly controlAdapter: SqlControlAdapter<'sqlite'> | undefined;
|
|
48
63
|
|
|
64
|
+
#endView = new MigrationContractViews<SqliteContractView<End>>(this, 'SqliteMigration', (json) =>
|
|
65
|
+
SqliteContractView.fromJson<End>(json),
|
|
66
|
+
);
|
|
67
|
+
#startView = new MigrationContractViews<SqliteContractView<Start>>(
|
|
68
|
+
this,
|
|
69
|
+
'SqliteMigration',
|
|
70
|
+
(json) => SqliteContractView.fromJson<Start>(json),
|
|
71
|
+
);
|
|
72
|
+
|
|
49
73
|
constructor(stack?: ControlStack<'sql', 'sqlite'>) {
|
|
50
74
|
super(stack);
|
|
51
75
|
this.controlAdapter = stack?.adapter
|
|
@@ -56,42 +80,63 @@ export abstract class SqliteMigration extends SqlMigration<SqlitePlanTargetDetai
|
|
|
56
80
|
: undefined;
|
|
57
81
|
}
|
|
58
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns the materialized control adapter, or throws a PN-MIG-2008 naming
|
|
85
|
+
* `operation` when the migration was constructed without a `ControlStack`.
|
|
86
|
+
* Single home for the null-check that every DDL/DML method shares.
|
|
87
|
+
*/
|
|
88
|
+
private controlAdapterFor(operation: string): SqlControlAdapter<'sqlite'> {
|
|
89
|
+
if (!this.controlAdapter) {
|
|
90
|
+
throw errorSqliteMigrationStackMissing(operation);
|
|
91
|
+
}
|
|
92
|
+
return this.controlAdapter;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The typed SQLite view over this migration's end-state contract — sole
|
|
97
|
+
* namespace unwrapped to the root, so `this.endContract.table.<name>` etc.
|
|
98
|
+
* Throws if no `endContractJson` was provided.
|
|
99
|
+
*/
|
|
100
|
+
get endContract(): SqliteContractView<End> {
|
|
101
|
+
return this.#endView.endContract;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The typed SQLite view over this migration's start-state contract, or
|
|
106
|
+
* `null` for a baseline migration (no `startContractJson`).
|
|
107
|
+
*/
|
|
108
|
+
get startContract(): SqliteContractView<Start> | null {
|
|
109
|
+
return this.#startView.startContract;
|
|
110
|
+
}
|
|
111
|
+
|
|
59
112
|
protected createTable(options: {
|
|
60
113
|
readonly table: string;
|
|
61
114
|
readonly ifNotExists?: boolean;
|
|
62
115
|
readonly columns: readonly DdlColumn[];
|
|
63
116
|
readonly constraints?: readonly DdlTableConstraint[];
|
|
64
117
|
}): Promise<Op> {
|
|
65
|
-
if (!this.controlAdapter) {
|
|
66
|
-
throw errorSqliteMigrationStackMissing();
|
|
67
|
-
}
|
|
68
118
|
return new CreateTableCall(options.table, options.columns, options.constraints).toOp(
|
|
69
|
-
this.
|
|
119
|
+
this.controlAdapterFor('createTable'),
|
|
70
120
|
);
|
|
71
121
|
}
|
|
72
122
|
|
|
73
123
|
protected dropTable(options: { readonly table: string }): Promise<Op> {
|
|
74
|
-
|
|
75
|
-
throw errorSqliteMigrationStackMissing();
|
|
76
|
-
}
|
|
77
|
-
return new DropTableCall(options.table).toOp(this.controlAdapter);
|
|
124
|
+
return new DropTableCall(options.table).toOp(this.controlAdapterFor('dropTable'));
|
|
78
125
|
}
|
|
79
126
|
|
|
80
127
|
protected addColumn(options: {
|
|
81
128
|
readonly table: string;
|
|
82
129
|
readonly column: SqliteColumnSpec;
|
|
83
130
|
}): Promise<Op> {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return new AddColumnCall(options.table, options.column).toOp(this.controlAdapter);
|
|
131
|
+
return new AddColumnCall(options.table, options.column).toOp(
|
|
132
|
+
this.controlAdapterFor('addColumn'),
|
|
133
|
+
);
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
protected dropColumn(options: { readonly table: string; readonly column: string }): Promise<Op> {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return new DropColumnCall(options.table, options.column).toOp(this.controlAdapter);
|
|
137
|
+
return new DropColumnCall(options.table, options.column).toOp(
|
|
138
|
+
this.controlAdapterFor('dropColumn'),
|
|
139
|
+
);
|
|
95
140
|
}
|
|
96
141
|
|
|
97
142
|
protected createIndex(options: {
|
|
@@ -99,19 +144,15 @@ export abstract class SqliteMigration extends SqlMigration<SqlitePlanTargetDetai
|
|
|
99
144
|
readonly index: string;
|
|
100
145
|
readonly columns: readonly string[];
|
|
101
146
|
}): Promise<Op> {
|
|
102
|
-
if (!this.controlAdapter) {
|
|
103
|
-
throw errorSqliteMigrationStackMissing();
|
|
104
|
-
}
|
|
105
147
|
return new CreateIndexCall(options.table, options.index, options.columns).toOp(
|
|
106
|
-
this.
|
|
148
|
+
this.controlAdapterFor('createIndex'),
|
|
107
149
|
);
|
|
108
150
|
}
|
|
109
151
|
|
|
110
152
|
protected dropIndex(options: { readonly table: string; readonly index: string }): Promise<Op> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return new DropIndexCall(options.table, options.index).toOp(this.controlAdapter);
|
|
153
|
+
return new DropIndexCall(options.table, options.index).toOp(
|
|
154
|
+
this.controlAdapterFor('dropIndex'),
|
|
155
|
+
);
|
|
115
156
|
}
|
|
116
157
|
|
|
117
158
|
protected recreateTable(options: {
|
|
@@ -123,9 +164,6 @@ export abstract class SqliteMigration extends SqlMigration<SqlitePlanTargetDetai
|
|
|
123
164
|
readonly postchecks: readonly { readonly description: string; readonly sql: string }[];
|
|
124
165
|
readonly operationClass: MigrationOperationClass;
|
|
125
166
|
}): Promise<Op> {
|
|
126
|
-
|
|
127
|
-
throw errorSqliteMigrationStackMissing();
|
|
128
|
-
}
|
|
129
|
-
return new RecreateTableCall(options).toOp(this.controlAdapter);
|
|
167
|
+
return new RecreateTableCall(options).toOp(this.controlAdapterFor('recreateTable'));
|
|
130
168
|
}
|
|
131
169
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import { SqlContractSerializerBase } from '@prisma-next/family-sql/ir';
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
3
|
+
import type { Namespace } from '@prisma-next/framework-components/ir';
|
|
4
|
+
import type { SqlNamespaceInput, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
5
5
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
6
|
-
import {
|
|
6
|
+
import type { JsonObject } from '@prisma-next/utils/json';
|
|
7
7
|
import { buildSqliteNamespace } from './sqlite-unbound-database';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -17,21 +17,36 @@ export class SqliteContractSerializer extends SqlContractSerializerBase<Contract
|
|
|
17
17
|
super(new Map());
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
protected override get defaultNamespaceId(): string {
|
|
21
|
-
return sqliteTargetDescriptorMeta.defaultNamespaceId;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
20
|
protected override hydrateSqlNamespaceEntry(
|
|
25
21
|
nsId: string,
|
|
26
|
-
raw:
|
|
27
|
-
): Namespace |
|
|
28
|
-
if (raw instanceof NamespaceBase) {
|
|
29
|
-
return raw;
|
|
30
|
-
}
|
|
22
|
+
raw: Record<string, unknown>,
|
|
23
|
+
): Namespace | SqlNamespaceInput {
|
|
31
24
|
const hydrated = blindCast<
|
|
32
|
-
|
|
33
|
-
'
|
|
25
|
+
SqlNamespaceInput,
|
|
26
|
+
'raw is always plain JSON, so super.hydrateSqlNamespaceEntry returns SqlNamespaceInput'
|
|
34
27
|
>(super.hydrateSqlNamespaceEntry(nsId, raw));
|
|
35
28
|
return buildSqliteNamespace(hydrated);
|
|
36
29
|
}
|
|
30
|
+
|
|
31
|
+
override serializeContract(contract: Contract<SqlStorage>): JsonObject {
|
|
32
|
+
const { storage, ...rest } = contract;
|
|
33
|
+
const namespacesJson: Record<string, JsonObject> = {};
|
|
34
|
+
for (const [nsId, ns] of Object.entries(storage.namespaces)) {
|
|
35
|
+
namespacesJson[nsId] = {
|
|
36
|
+
id: ns.id,
|
|
37
|
+
entries: this.serializeNamespaceEntries(ns.entries),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return blindCast<
|
|
41
|
+
JsonObject,
|
|
42
|
+
'rest + storage are serialized plain values; spread preserves JSON-clean contract envelope'
|
|
43
|
+
>({
|
|
44
|
+
...rest,
|
|
45
|
+
storage: {
|
|
46
|
+
storageHash: String(storage.storageHash),
|
|
47
|
+
namespaces: namespacesJson,
|
|
48
|
+
...(storage.types !== undefined ? { types: this.serializeJsonObject(storage.types) } : {}),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
37
52
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import {
|
|
3
|
+
buildSqlSingleNamespaceView,
|
|
4
|
+
type SqlSingleNamespaceView,
|
|
5
|
+
} from '@prisma-next/sql-contract/contract-view';
|
|
6
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
7
|
+
import { SqliteContractSerializer } from './sqlite-contract-serializer';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A SQLite contract view: the deserialized contract intersected with by-name
|
|
11
|
+
* accessors. It is substitutable for `Contract` (carries `storage`, `domain`,
|
|
12
|
+
* …) and also exposes the single default namespace unwrapped with the SQL
|
|
13
|
+
* built-in kinds promoted:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const view = SqliteContractView.fromJson<Contract>(contractJson);
|
|
17
|
+
* view.table.users // typed table leaf
|
|
18
|
+
* view.entries.policy.X // pack-contributed kind (singular key)
|
|
19
|
+
* view.storage // the full contract is still present
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* SQLite has `sql.enums: false`, so it never emits `valueSet` entries; the
|
|
23
|
+
* `valueSet` slot is therefore an empty map.
|
|
24
|
+
*/
|
|
25
|
+
export type SqliteContractView<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> =
|
|
26
|
+
SqlSingleNamespaceView<TContract>;
|
|
27
|
+
|
|
28
|
+
export const SqliteContractView = {
|
|
29
|
+
/** Wrap an already-deserialized SQLite contract in a view. */
|
|
30
|
+
from<TContract extends Contract<SqlStorage>>(contract: TContract): SqliteContractView<TContract> {
|
|
31
|
+
return buildSqlSingleNamespaceView(contract);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/** Deserialize a SQLite contract JSON envelope and wrap it in a view. */
|
|
35
|
+
fromJson<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>(
|
|
36
|
+
json: unknown,
|
|
37
|
+
): SqliteContractView<TContract> {
|
|
38
|
+
const contract = new SqliteContractSerializer().deserializeContract<TContract>(json);
|
|
39
|
+
return buildSqlSingleNamespaceView(contract);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import { SqlSchemaVerifierBase } from '@prisma-next/family-sql/ir';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
SchemaDiffIssue,
|
|
5
|
+
SchemaVerifyOptions,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
4
7
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
5
8
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
6
9
|
|
|
@@ -12,13 +15,13 @@ import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
|
12
15
|
export class SqliteSchemaVerifier extends SqlSchemaVerifierBase<Contract<SqlStorage>, SqlSchemaIR> {
|
|
13
16
|
protected verifyCommonSqlSchema(
|
|
14
17
|
_options: SchemaVerifyOptions<Contract<SqlStorage>, SqlSchemaIR>,
|
|
15
|
-
): readonly
|
|
18
|
+
): readonly SchemaDiffIssue[] {
|
|
16
19
|
return [];
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
protected verifyTargetExtensions(
|
|
20
23
|
_options: SchemaVerifyOptions<Contract<SqlStorage>, SqlSchemaIR>,
|
|
21
|
-
): readonly
|
|
24
|
+
): readonly SchemaDiffIssue[] {
|
|
22
25
|
return [];
|
|
23
26
|
}
|
|
24
27
|
}
|