@prisma-next/target-sqlite 0.14.0 → 0.15.0-dev.10

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.
Files changed (82) hide show
  1. package/dist/codecs-BGeJavlQ.d.mts.map +1 -1
  2. package/dist/codecs.mjs +220 -2
  3. package/dist/codecs.mjs.map +1 -1
  4. package/dist/contract-free.mjs +1 -1
  5. package/dist/{control-tables-7KwMyJ6i.mjs → control-tables-D_o8dKRB.mjs} +2 -2
  6. package/dist/{control-tables-7KwMyJ6i.mjs.map → control-tables-D_o8dKRB.mjs.map} +1 -1
  7. package/dist/control-tables.mjs +1 -1
  8. package/dist/control.d.mts +3 -4
  9. package/dist/control.d.mts.map +1 -1
  10. package/dist/control.mjs +17 -28
  11. package/dist/control.mjs.map +1 -1
  12. package/dist/{descriptor-meta-DxmEeTJ-.mjs → descriptor-meta-D1fap-5t.mjs} +4 -11
  13. package/dist/descriptor-meta-D1fap-5t.mjs.map +1 -0
  14. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs +12 -0
  15. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs.map +1 -0
  16. package/dist/migration.d.mts +2 -2
  17. package/dist/migration.mjs +2 -2
  18. package/dist/native-type-normalizer.mjs +13 -1
  19. package/dist/native-type-normalizer.mjs.map +1 -0
  20. package/dist/{op-factory-call-DmdfD1yd.mjs → op-factory-call-BlMqc5G_.mjs} +134 -117
  21. package/dist/op-factory-call-BlMqc5G_.mjs.map +1 -0
  22. package/dist/op-factory-call.d.mts +2 -2
  23. package/dist/op-factory-call.d.mts.map +1 -1
  24. package/dist/op-factory-call.mjs +1 -1
  25. package/dist/pack.mjs +1 -1
  26. package/dist/planner-CxbLEZHF.mjs +815 -0
  27. package/dist/planner-CxbLEZHF.mjs.map +1 -0
  28. package/dist/{planner-produced-sqlite-migration-0xPEm3R1.mjs → planner-produced-sqlite-migration-D5415zDp.mjs} +46 -17
  29. package/dist/planner-produced-sqlite-migration-D5415zDp.mjs.map +1 -0
  30. package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts → planner-produced-sqlite-migration-DKopa0rd.d.mts} +3 -3
  31. package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts.map → planner-produced-sqlite-migration-DKopa0rd.d.mts.map} +1 -1
  32. package/dist/planner-produced-sqlite-migration.d.mts +1 -1
  33. package/dist/planner-produced-sqlite-migration.mjs +1 -1
  34. package/dist/planner.d.mts +47 -11
  35. package/dist/planner.d.mts.map +1 -1
  36. package/dist/planner.mjs +1 -1
  37. package/dist/runtime.d.mts +6 -4
  38. package/dist/runtime.d.mts.map +1 -1
  39. package/dist/runtime.mjs +3 -3
  40. package/dist/{shared-Dhc8mLK1.d.mts → shared-CaiRyjA0.d.mts} +2 -3
  41. package/dist/{shared-Dhc8mLK1.d.mts.map → shared-CaiRyjA0.d.mts.map} +1 -1
  42. package/dist/{sqlite-contract-serializer--iaDgC8e.mjs → sqlite-contract-view-B8qeCczc.mjs} +35 -10
  43. package/dist/sqlite-contract-view-B8qeCczc.mjs.map +1 -0
  44. package/dist/sqlite-contract-view-J32K82Gi.d.mts +29 -0
  45. package/dist/sqlite-contract-view-J32K82Gi.d.mts.map +1 -0
  46. package/dist/{sqlite-migration-DVfhQwN_.d.mts → sqlite-migration-Dzz9p0Gw.d.mts} +35 -6
  47. package/dist/sqlite-migration-Dzz9p0Gw.d.mts.map +1 -0
  48. package/dist/sqlite-migration-WdRHavfF.mjs +123 -0
  49. package/dist/sqlite-migration-WdRHavfF.mjs.map +1 -0
  50. package/package.json +20 -20
  51. package/src/core/codecs.ts +10 -3
  52. package/src/core/control-target.ts +13 -18
  53. package/src/core/errors.ts +14 -12
  54. package/src/core/migrations/column-ddl-rendering.ts +177 -0
  55. package/src/core/migrations/diff-database-schema.ts +230 -0
  56. package/src/core/migrations/issue-planner.ts +333 -498
  57. package/src/core/migrations/op-factory-call.ts +5 -0
  58. package/src/core/migrations/operations/shared.ts +3 -5
  59. package/src/core/migrations/operations/tables.ts +108 -68
  60. package/src/core/migrations/planner-strategies.ts +119 -137
  61. package/src/core/migrations/planner.ts +118 -30
  62. package/src/core/migrations/render-typescript.ts +42 -15
  63. package/src/core/migrations/runner.ts +4 -10
  64. package/src/core/migrations/sqlite-migration.ts +69 -31
  65. package/src/core/sqlite-contract-serializer.ts +29 -14
  66. package/src/core/sqlite-contract-view.ts +41 -0
  67. package/src/core/sqlite-schema-verifier.ts +6 -3
  68. package/src/core/sqlite-unbound-database.ts +8 -8
  69. package/src/exports/runtime.ts +1 -0
  70. package/dist/codecs-DsC4OGmU.mjs +0 -220
  71. package/dist/codecs-DsC4OGmU.mjs.map +0 -1
  72. package/dist/descriptor-meta-DxmEeTJ-.mjs.map +0 -1
  73. package/dist/native-type-normalizer-CiSyVmMP.mjs +0 -14
  74. package/dist/native-type-normalizer-CiSyVmMP.mjs.map +0 -1
  75. package/dist/op-factory-call-DmdfD1yd.mjs.map +0 -1
  76. package/dist/planner-Ciq8p_dL.mjs +0 -622
  77. package/dist/planner-Ciq8p_dL.mjs.map +0 -1
  78. package/dist/planner-produced-sqlite-migration-0xPEm3R1.mjs.map +0 -1
  79. package/dist/sqlite-contract-serializer--iaDgC8e.mjs.map +0 -1
  80. package/dist/sqlite-migration-A0rwqPOG.mjs +0 -92
  81. package/dist/sqlite-migration-A0rwqPOG.mjs.map +0 -1
  82. package/dist/sqlite-migration-DVfhQwN_.d.mts.map +0 -1
@@ -11,16 +11,18 @@ 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
- SchemaIssue,
18
+ SchemaDiffIssue,
19
+ SchemaOwnership,
20
20
  } from '@prisma-next/framework-components/control';
21
- import { parseSqliteDefault } from '../default-normalizer';
22
- import { normalizeSqliteNativeType } from '../native-type-normalizer';
23
- import { planIssues } from './issue-planner';
21
+ import { issueOutcome } from '@prisma-next/framework-components/control';
22
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
23
+ import { RelationalSchemaNodeKind, type SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
24
+ import { buildSqlitePlanDiff } from './diff-database-schema';
25
+ import { coalesceSubtreeIssues, issueNode, planIssues } from './issue-planner';
24
26
  import {
25
27
  type SqliteMigrationDestinationInfo,
26
28
  TypeScriptRenderableSqliteMigration,
@@ -41,16 +43,18 @@ export type SqlitePlanResult =
41
43
  /**
42
44
  * SQLite migration planner — a thin wrapper over `planIssues`.
43
45
  *
44
- * `plan()` verifies the live schema against the target contract (producing
45
- * `SchemaIssue[]`) and delegates to `planIssues` with the registered
46
- * strategies. Strategies absorb groups of related issues into composite
47
- * recipes (e.g. recreating a table to apply type/nullability/default/
48
- * constraint changes at once); anything not absorbed by a strategy flows
49
- * through `mapIssueToCall` in the issue planner as a one-off call.
46
+ * `plan()` diffs the live schema against the target contract via the one
47
+ * differ (`buildSqlitePlanDiff`, producing node-typed `SchemaDiffIssue[]`)
48
+ * and delegates to `planIssues` with the registered strategies. Strategies
49
+ * absorb groups of related issues into composite recipes (e.g. recreating a
50
+ * table to apply type/nullability/default/constraint changes at once);
51
+ * anything not absorbed by a strategy flows through `mapNodeIssueToCall` in
52
+ * the issue planner as a one-off call.
50
53
  *
51
- * FK-backing indexes are surfaced by `verifySqlSchema`'s index expansion
52
- * (see `verify-sql-schema.ts:459-469`), so `mapIssueToCall` handles them
53
- * uniformly alongside user-declared indexes.
54
+ * FK-backing indexes are already merged into the expected table node's
55
+ * `indexes` at derivation (`contractToSchemaIR`'s `convertTable`), so
56
+ * `mapNodeIssueToCall` handles them uniformly alongside user-declared
57
+ * indexes — no separate expansion step in the planner.
54
58
  */
55
59
  export class SqliteMigrationPlanner
56
60
  implements SqlMigrationPlanner<SqlitePlanTargetDetails>, MigrationPlanner<'sql', 'sqlite'>
@@ -83,6 +87,11 @@ export class SqliteMigrationPlanner
83
87
  * the marker row by the right space.
84
88
  */
85
89
  readonly spaceId: string;
90
+ /**
91
+ * Ownership oracle over the contract-space composition — see
92
+ * {@link SqlMigrationPlannerPlanOptions.ownership}.
93
+ */
94
+ readonly ownership?: SchemaOwnership;
86
95
  }): SqlitePlanResult {
87
96
  return this.planSql(options as SqlMigrationPlannerPlanOptions);
88
97
  }
@@ -107,17 +116,13 @@ export class SqliteMigrationPlanner
107
116
  const policyResult = this.ensureAdditivePolicy(options.policy);
108
117
  if (policyResult) return policyResult;
109
118
 
110
- const schemaIssues = this.collectSchemaIssues(options);
119
+ const { expected, actual, issues } = this.collectSchemaIssues(options);
111
120
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
112
- const storageTypes = options.contract.storage.types ?? {};
113
121
 
114
122
  const result = planIssues({
115
- issues: schemaIssues,
116
- toContract: options.contract,
117
- fromContract: options.fromContract,
118
- codecHooks,
119
- storageTypes,
120
- schema: options.schema,
123
+ issues,
124
+ expected,
125
+ actual,
121
126
  policy: options.policy,
122
127
  frameworkComponents: options.frameworkComponents,
123
128
  strategies: sqlitePlannerStrategies,
@@ -178,18 +183,101 @@ export class SqliteMigrationPlanner
178
183
  return null;
179
184
  }
180
185
 
181
- private collectSchemaIssues(options: SqlMigrationPlannerPlanOptions): readonly SchemaIssue[] {
186
+ /**
187
+ * Diffs the target contract against the live schema via the one differ
188
+ * (the same tree-building `diffSqliteSchema` uses, plus the
189
+ * op-render stamper) and prepares the issue list `planIssues` consumes.
190
+ *
191
+ * Three passes, in order:
192
+ * 1. Subtree coalescing — the differ is total (a missing/extra table also
193
+ * emits an issue for every column/constraint under it); those nested
194
+ * issues are redundant once the table-level `CreateTable`/`DropTable`
195
+ * call already accounts for the whole subtree. Runs FIRST, over the
196
+ * complete diff: a sibling-owned extra table's column issues must
197
+ * collapse into its one table-level issue before the ownership pass,
198
+ * because a bare column node carries no table reference — if coalescing
199
+ * ran after ownership dropped the table-level issue, the orphaned column
200
+ * issues would survive and the planner would emit drops against a
201
+ * sibling space's table.
202
+ * 2. Ownership — a live extra is only this plan's to drop when no contract
203
+ * space owns it. The differ ran against THIS space's contract, so a
204
+ * table a sibling owns surfaces here as `not-expected`; the planner asks
205
+ * the ownership oracle (the passive aggregate) whether any space declares
206
+ * it and, if so, leaves it alone. A table no space owns stays a genuine
207
+ * extra. Ownership lives in the aggregate; the planner only asks. No
208
+ * oracle (single-space) keeps every extra.
209
+ * 3. Strict-mode extras gating — `not-expected` (extra table/column/
210
+ * constraint) issues are dropped entirely outside strict mode, mirroring
211
+ * the retired coordinate walk's `if (strict) { ...extra_* } }` guards:
212
+ * an additive-only plan must never even consider dropping an unclaimed
213
+ * object, not just refuse to emit the drop.
214
+ */
215
+ private collectSchemaIssues(options: SqlMigrationPlannerPlanOptions): {
216
+ readonly expected: SqlSchemaIR;
217
+ readonly actual: SqlSchemaIR;
218
+ readonly issues: readonly SchemaDiffIssue[];
219
+ } {
182
220
  const allowed = options.policy.allowedOperationClasses;
183
221
  const strict = allowed.includes('widening') || allowed.includes('destructive');
184
- const verifyResult = verifySqlSchema({
222
+ const {
223
+ expected,
224
+ actual,
225
+ issues: rawIssues,
226
+ } = buildSqlitePlanDiff({
185
227
  contract: options.contract,
186
- schema: options.schema,
187
- strict,
188
- typeMetadataRegistry: new Map(),
228
+ actualSchema: options.schema,
189
229
  frameworkComponents: options.frameworkComponents,
190
- normalizeDefault: parseSqliteDefault,
191
- normalizeNativeType: normalizeSqliteNativeType,
192
230
  });
193
- return verifyResult.schema.issues;
231
+ const coalesced = coalesceSubtreeIssues(rawIssues);
232
+ const owned = retainUnownedExtras(coalesced, options.ownership);
233
+ const issues = strict ? owned : owned.filter((issue) => issueOutcome(issue) !== 'not-expected');
234
+ return { expected, actual, issues };
194
235
  }
195
236
  }
237
+
238
+ /**
239
+ * Drops a `not-expected` issue when it is a whole extra TABLE that some
240
+ * contract space owns, asking the ownership oracle per node.
241
+ *
242
+ * The consultation applies ONLY to table-level extras — a live table this
243
+ * space's contract lacks — identified by asking the issue's own node
244
+ * (`nodeKind === table`), never by counting path segments. SQLite is a
245
+ * single-namespace target, so every coordinate is qualified with the shared
246
+ * `UNBOUND_NAMESPACE_ID` sentinel (the same one the aggregate's declared
247
+ * entities carry for this target). `entityKind` is the literal `'table'`:
248
+ * this function only ever asks about a node already confirmed to be
249
+ * `RelationalSchemaNodeKind.table` (checked just above), and that's a
250
+ * diff-tree `nodeKind` spelling (`'sql-table'`) distinct from the storage
251
+ * `entries` vocabulary `elementCoordinates` walks (`'table'`) — the literal
252
+ * is that storage-entries spelling. `declaresEntity` answers over the whole
253
+ * composition (self included), so a positive answer on such a table means
254
+ * another space owns it (a table this space owned would be in its expected
255
+ * tree, never an extra): leave it. A negative answer means no space owns
256
+ * it — a genuine orphan to drop.
257
+ *
258
+ * A DEEPER extra (an extra column/constraint on a table this space DOES own —
259
+ * only the child drifted, so the table is in the expected tree) is this
260
+ * space's own drift and is always kept for dropping; asking the oracle there
261
+ * would wrongly suppress it, because the owned table answers `true`. No oracle
262
+ * ⇒ every extra is kept (single-space plan).
263
+ */
264
+ function retainUnownedExtras(
265
+ issues: readonly SchemaDiffIssue[],
266
+ ownership: SchemaOwnership | undefined,
267
+ ): readonly SchemaDiffIssue[] {
268
+ if (ownership === undefined) return issues;
269
+ return issues.filter((issue) => {
270
+ if (issueOutcome(issue) !== 'not-expected') return true;
271
+ const node = issueNode(issue);
272
+ if (node === undefined || node.nodeKind !== RelationalSchemaNodeKind.table) return true;
273
+ const tableName = issue.path[1];
274
+ return (
275
+ tableName === undefined ||
276
+ !ownership.declaresEntity({
277
+ namespaceId: UNBOUND_NAMESPACE_ID,
278
+ entityKind: 'table',
279
+ entityName: tableName,
280
+ })
281
+ );
282
+ });
283
+ }
@@ -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
- 'export default class M extends Migration {',
46
- buildDescribeMethod(meta),
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>): string {
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
- function buildDescribeMethod(meta: RenderMigrationMeta): string {
70
- const lines: string[] = [];
71
- lines.push(' override describe() {');
72
- lines.push(' return {');
73
- lines.push(` from: ${JSON.stringify(meta.from)},`);
74
- lines.push(` to: ${JSON.stringify(meta.to)},`);
75
- lines.push(' };');
76
- lines.push(' }');
77
- lines.push('');
78
- return lines.join('\n');
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 { parseSqliteDefault } from '../default-normalizer';
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 schemaIR = await this.family.introspect({
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
- schema: schemaIR,
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 extends SqlMigration<SqlitePlanTargetDetails, 'sqlite'> {
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); the operation methods
45
- * throw in that case to surface the misuse.
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.controlAdapter,
119
+ this.controlAdapterFor('createTable'),
70
120
  );
71
121
  }
72
122
 
73
123
  protected dropTable(options: { readonly table: string }): Promise<Op> {
74
- if (!this.controlAdapter) {
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
- if (!this.controlAdapter) {
85
- throw errorSqliteMigrationStackMissing();
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
- if (!this.controlAdapter) {
92
- throw errorSqliteMigrationStackMissing();
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.controlAdapter,
148
+ this.controlAdapterFor('createIndex'),
107
149
  );
108
150
  }
109
151
 
110
152
  protected dropIndex(options: { readonly table: string; readonly index: string }): Promise<Op> {
111
- if (!this.controlAdapter) {
112
- throw errorSqliteMigrationStackMissing();
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
- if (!this.controlAdapter) {
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 { type Namespace, NamespaceBase } from '@prisma-next/framework-components/ir';
4
- import type { SqlNamespaceTablesInput, SqlStorage } from '@prisma-next/sql-contract/types';
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 { sqliteTargetDescriptorMeta } from './descriptor-meta';
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: Namespace | Record<string, unknown>,
27
- ): Namespace | SqlNamespaceTablesInput {
28
- if (raw instanceof NamespaceBase) {
29
- return raw;
30
- }
22
+ raw: Record<string, unknown>,
23
+ ): Namespace | SqlNamespaceInput {
31
24
  const hydrated = blindCast<
32
- SqlNamespaceTablesInput,
33
- 'super.hydrateSqlNamespaceEntry returns the tables form when raw is not a NamespaceBase'
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 { SchemaIssue, SchemaVerifyOptions } from '@prisma-next/framework-components/control';
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 SchemaIssue[] {
18
+ ): readonly SchemaDiffIssue[] {
16
19
  return [];
17
20
  }
18
21
 
19
22
  protected verifyTargetExtensions(
20
23
  _options: SchemaVerifyOptions<Contract<SqlStorage>, SqlSchemaIR>,
21
- ): readonly SchemaIssue[] {
24
+ ): readonly SchemaDiffIssue[] {
22
25
  return [];
23
26
  }
24
27
  }