@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.
Files changed (72) hide show
  1. package/dist/contract-free.mjs +1 -1
  2. package/dist/control.d.mts +3 -4
  3. package/dist/control.d.mts.map +1 -1
  4. package/dist/control.mjs +16 -27
  5. package/dist/control.mjs.map +1 -1
  6. package/dist/{descriptor-meta-DxmEeTJ-.mjs → descriptor-meta-D1fap-5t.mjs} +4 -11
  7. package/dist/descriptor-meta-D1fap-5t.mjs.map +1 -0
  8. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs +12 -0
  9. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs.map +1 -0
  10. package/dist/migration.d.mts +2 -2
  11. package/dist/migration.mjs +2 -2
  12. package/dist/native-type-normalizer.mjs +13 -1
  13. package/dist/native-type-normalizer.mjs.map +1 -0
  14. package/dist/{op-factory-call-DmdfD1yd.mjs → op-factory-call-Do03QgD9.mjs} +134 -117
  15. package/dist/op-factory-call-Do03QgD9.mjs.map +1 -0
  16. package/dist/op-factory-call.d.mts +2 -2
  17. package/dist/op-factory-call.d.mts.map +1 -1
  18. package/dist/op-factory-call.mjs +1 -1
  19. package/dist/pack.mjs +1 -1
  20. package/dist/planner-DGhzXuGh.mjs +848 -0
  21. package/dist/planner-DGhzXuGh.mjs.map +1 -0
  22. package/dist/{planner-produced-sqlite-migration-0xPEm3R1.mjs → planner-produced-sqlite-migration-BDzhFAoW.mjs} +46 -17
  23. package/dist/planner-produced-sqlite-migration-BDzhFAoW.mjs.map +1 -0
  24. package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts → planner-produced-sqlite-migration-o1qUzwLk.d.mts} +3 -3
  25. package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts.map → planner-produced-sqlite-migration-o1qUzwLk.d.mts.map} +1 -1
  26. package/dist/planner-produced-sqlite-migration.d.mts +1 -1
  27. package/dist/planner-produced-sqlite-migration.mjs +1 -1
  28. package/dist/planner.d.mts +47 -11
  29. package/dist/planner.d.mts.map +1 -1
  30. package/dist/planner.mjs +1 -1
  31. package/dist/runtime.d.mts +6 -4
  32. package/dist/runtime.d.mts.map +1 -1
  33. package/dist/runtime.mjs +3 -3
  34. package/dist/{shared-Dhc8mLK1.d.mts → shared-C6sM-wel.d.mts} +2 -2
  35. package/dist/{shared-Dhc8mLK1.d.mts.map → shared-C6sM-wel.d.mts.map} +1 -1
  36. package/dist/{sqlite-contract-serializer--iaDgC8e.mjs → sqlite-contract-view-BEwi0mUC.mjs} +34 -9
  37. package/dist/sqlite-contract-view-BEwi0mUC.mjs.map +1 -0
  38. package/dist/sqlite-contract-view-J32K82Gi.d.mts +29 -0
  39. package/dist/sqlite-contract-view-J32K82Gi.d.mts.map +1 -0
  40. package/dist/sqlite-migration-BNiSiP0z.mjs +123 -0
  41. package/dist/sqlite-migration-BNiSiP0z.mjs.map +1 -0
  42. package/dist/{sqlite-migration-DVfhQwN_.d.mts → sqlite-migration-BYbzskpE.d.mts} +35 -6
  43. package/dist/sqlite-migration-BYbzskpE.d.mts.map +1 -0
  44. package/package.json +18 -18
  45. package/src/core/control-target.ts +13 -18
  46. package/src/core/errors.ts +14 -12
  47. package/src/core/migrations/column-ddl-rendering.ts +177 -0
  48. package/src/core/migrations/diff-database-schema.ts +230 -0
  49. package/src/core/migrations/issue-planner.ts +361 -488
  50. package/src/core/migrations/op-factory-call.ts +5 -0
  51. package/src/core/migrations/operations/tables.ts +107 -68
  52. package/src/core/migrations/planner-strategies.ts +118 -137
  53. package/src/core/migrations/planner.ts +117 -30
  54. package/src/core/migrations/render-typescript.ts +42 -15
  55. package/src/core/migrations/runner.ts +4 -10
  56. package/src/core/migrations/sqlite-migration.ts +69 -31
  57. package/src/core/sqlite-contract-serializer.ts +29 -14
  58. package/src/core/sqlite-contract-view.ts +41 -0
  59. package/src/core/sqlite-schema-verifier.ts +6 -3
  60. package/src/core/sqlite-unbound-database.ts +8 -8
  61. package/src/exports/runtime.ts +1 -0
  62. package/dist/descriptor-meta-DxmEeTJ-.mjs.map +0 -1
  63. package/dist/native-type-normalizer-CiSyVmMP.mjs +0 -14
  64. package/dist/native-type-normalizer-CiSyVmMP.mjs.map +0 -1
  65. package/dist/op-factory-call-DmdfD1yd.mjs.map +0 -1
  66. package/dist/planner-Ciq8p_dL.mjs +0 -622
  67. package/dist/planner-Ciq8p_dL.mjs.map +0 -1
  68. package/dist/planner-produced-sqlite-migration-0xPEm3R1.mjs.map +0 -1
  69. package/dist/sqlite-contract-serializer--iaDgC8e.mjs.map +0 -1
  70. package/dist/sqlite-migration-A0rwqPOG.mjs +0 -92
  71. package/dist/sqlite-migration-A0rwqPOG.mjs.map +0 -1
  72. 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
- 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 { 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()` 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.
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 surfaced by `verifySqlSchema`'s index expansion
52
- * (see `verify-sql-schema.ts:459-469`), so `mapIssueToCall` handles them
53
- * uniformly alongside user-declared indexes.
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 schemaIssues = this.collectSchemaIssues(options);
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: schemaIssues,
116
- toContract: options.contract,
117
- fromContract: options.fromContract,
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
- private collectSchemaIssues(options: SqlMigrationPlannerPlanOptions): readonly SchemaIssue[] {
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 verifyResult = verifySqlSchema({
221
+ const {
222
+ expected,
223
+ actual,
224
+ issues: rawIssues,
225
+ } = buildSqlitePlanDiff({
185
226
  contract: options.contract,
186
- schema: options.schema,
187
- strict,
188
- typeMetadataRegistry: new Map(),
227
+ actualSchema: options.schema,
189
228
  frameworkComponents: options.frameworkComponents,
190
- normalizeDefault: parseSqliteDefault,
191
- normalizeNativeType: normalizeSqliteNativeType,
192
229
  });
193
- return verifyResult.schema.issues;
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
- '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
  }