@prisma-next/target-sqlite 0.13.0 → 0.14.0-dev.2

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 (86) hide show
  1. package/dist/contract-free.d.mts +35 -2
  2. package/dist/contract-free.d.mts.map +1 -1
  3. package/dist/contract-free.mjs +3 -22
  4. package/dist/contract-free.mjs.map +1 -1
  5. package/dist/control.d.mts +5 -8
  6. package/dist/control.d.mts.map +1 -1
  7. package/dist/control.mjs +18 -20
  8. package/dist/control.mjs.map +1 -1
  9. package/dist/ddl-DrtjQMFK.mjs +68 -0
  10. package/dist/ddl-DrtjQMFK.mjs.map +1 -0
  11. package/dist/{descriptor-meta-Dxx2A6PT.mjs → descriptor-meta-DxmEeTJ-.mjs} +10 -3
  12. package/dist/descriptor-meta-DxmEeTJ-.mjs.map +1 -0
  13. package/dist/migration.d.mts +4 -46
  14. package/dist/migration.d.mts.map +1 -1
  15. package/dist/migration.mjs +4 -3
  16. package/dist/migration.mjs.map +1 -1
  17. package/dist/op-factory-call-DmdfD1yd.mjs +794 -0
  18. package/dist/op-factory-call-DmdfD1yd.mjs.map +1 -0
  19. package/dist/op-factory-call.d.mts +22 -12
  20. package/dist/op-factory-call.d.mts.map +1 -1
  21. package/dist/op-factory-call.mjs +1 -1
  22. package/dist/pack.mjs +1 -1
  23. package/dist/{planner-DSNDwQy9.mjs → planner-Ciq8p_dL.mjs} +80 -12
  24. package/dist/planner-Ciq8p_dL.mjs.map +1 -0
  25. package/dist/{planner-produced-sqlite-migration-DowV_vHw.mjs → planner-produced-sqlite-migration-0xPEm3R1.mjs} +9 -5
  26. package/dist/planner-produced-sqlite-migration-0xPEm3R1.mjs.map +1 -0
  27. package/dist/{planner-produced-sqlite-migration-C1yqJAiM.d.mts → planner-produced-sqlite-migration-CpgsY-M9.d.mts} +5 -4
  28. package/dist/planner-produced-sqlite-migration-CpgsY-M9.d.mts.map +1 -0
  29. package/dist/planner-produced-sqlite-migration.d.mts +1 -1
  30. package/dist/planner-produced-sqlite-migration.mjs +1 -1
  31. package/dist/planner.d.mts +5 -2
  32. package/dist/planner.d.mts.map +1 -1
  33. package/dist/planner.mjs +1 -1
  34. package/dist/render-ops-BDW2tUeR.mjs +22 -0
  35. package/dist/render-ops-BDW2tUeR.mjs.map +1 -0
  36. package/dist/render-ops.d.mts +2 -1
  37. package/dist/render-ops.d.mts.map +1 -1
  38. package/dist/render-ops.mjs +1 -1
  39. package/dist/runtime.d.mts +1 -0
  40. package/dist/runtime.d.mts.map +1 -1
  41. package/dist/runtime.mjs +2 -2
  42. package/dist/shared-Dhc8mLK1.d.mts.map +1 -1
  43. package/dist/{sqlite-contract-serializer-jcRu8aHh.mjs → sqlite-contract-serializer--iaDgC8e.mjs} +17 -6
  44. package/dist/sqlite-contract-serializer--iaDgC8e.mjs.map +1 -0
  45. package/dist/sqlite-migration-A0rwqPOG.mjs +92 -0
  46. package/dist/sqlite-migration-A0rwqPOG.mjs.map +1 -0
  47. package/dist/sqlite-migration-DVfhQwN_.d.mts +75 -0
  48. package/dist/sqlite-migration-DVfhQwN_.d.mts.map +1 -0
  49. package/package.json +18 -18
  50. package/src/contract-free/checks.ts +75 -0
  51. package/src/core/control-target.ts +4 -4
  52. package/src/core/errors.ts +28 -0
  53. package/src/core/migrations/issue-planner.ts +151 -8
  54. package/src/core/migrations/op-factory-call.ts +332 -45
  55. package/src/core/migrations/operations/columns.ts +32 -26
  56. package/src/core/migrations/operations/indexes.ts +31 -27
  57. package/src/core/migrations/operations/shared.ts +11 -3
  58. package/src/core/migrations/operations/tables.ts +39 -37
  59. package/src/core/migrations/planner-ddl-builders.ts +7 -16
  60. package/src/core/migrations/planner-produced-sqlite-migration.ts +8 -2
  61. package/src/core/migrations/planner-strategies.ts +3 -3
  62. package/src/core/migrations/planner.ts +14 -2
  63. package/src/core/migrations/render-ops.ts +37 -9
  64. package/src/core/migrations/runner.ts +16 -12
  65. package/src/core/migrations/sqlite-migration.ts +119 -1
  66. package/src/core/sqlite-contract-serializer.ts +5 -0
  67. package/src/core/sqlite-unbound-database.ts +30 -54
  68. package/src/exports/contract-free.ts +8 -0
  69. package/src/exports/migration.ts +8 -3
  70. package/dist/descriptor-meta-Dxx2A6PT.mjs.map +0 -1
  71. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs +0 -12
  72. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs.map +0 -1
  73. package/dist/op-factory-call-DymqdXQW.mjs +0 -279
  74. package/dist/op-factory-call-DymqdXQW.mjs.map +0 -1
  75. package/dist/planner-DSNDwQy9.mjs.map +0 -1
  76. package/dist/planner-produced-sqlite-migration-C1yqJAiM.d.mts.map +0 -1
  77. package/dist/planner-produced-sqlite-migration-DowV_vHw.mjs.map +0 -1
  78. package/dist/render-ops-CFRbJ3Yb.mjs +0 -8
  79. package/dist/render-ops-CFRbJ3Yb.mjs.map +0 -1
  80. package/dist/sqlite-contract-serializer-jcRu8aHh.mjs.map +0 -1
  81. package/dist/sqlite-migration-CUqgmzQH.mjs +0 -16
  82. package/dist/sqlite-migration-CUqgmzQH.mjs.map +0 -1
  83. package/dist/sqlite-migration-D4XGYzgQ.d.mts +0 -17
  84. package/dist/sqlite-migration-D4XGYzgQ.d.mts.map +0 -1
  85. package/dist/tables-CjB7vXCr.mjs +0 -412
  86. package/dist/tables-CjB7vXCr.mjs.map +0 -1
@@ -2,13 +2,11 @@
2
2
  * SQLite migration IR: one concrete `*Call` class per pure factory under
3
3
  * `operations/`, plus a shared `SqliteOpFactoryCallNode` abstract base.
4
4
  *
5
- * Each call class carries fully-resolved literal arguments (flat
6
- * `SqliteColumnSpec` / `SqliteTableSpec` etc.) codec / `typeRef` / default
7
- * expansion happens upstream in the issue-planner / strategies, mirroring
8
- * the Postgres `ColumnSpec` pattern. As a result, `toOp()` and
9
- * `renderTypeScript()` both pass the same flat data through; the rendered
10
- * TypeScript scaffold is fully self-contained and does not need access to a
11
- * `storageTypes` map at runtime.
5
+ * Each call class carries fully-resolved literal arguments. `CreateTableCall`
6
+ * holds structured `DdlColumn[]` + `DdlTableConstraint[]` and lowers via the
7
+ * adapter's DDL path; other call classes carry flat SQL fragments. Codec /
8
+ * `typeRef` / default expansion happens upstream in the issue-planner /
9
+ * strategies, mirroring the Postgres `ColumnSpec` pattern.
12
10
  */
13
11
 
14
12
  import { errorUnfilledPlaceholder } from '@prisma-next/errors/migration';
@@ -16,13 +14,25 @@ import type {
16
14
  MigrationOperationClass,
17
15
  SqlMigrationPlanOperation,
18
16
  } from '@prisma-next/family-sql/control';
17
+ import type { ExecuteRequestLowerer, Lowerer } from '@prisma-next/family-sql/control-adapter';
19
18
  import type { OpFactoryCall as FrameworkOpFactoryCall } from '@prisma-next/framework-components/control';
19
+ import type {
20
+ AnyDdlColumnDefault,
21
+ DdlColumn,
22
+ DdlTableConstraint,
23
+ } from '@prisma-next/sql-relational-core/ast';
20
24
  import { type ImportRequirement, jsonToTsSource, TsExpression } from '@prisma-next/ts-render';
21
- import { addColumn, dropColumn } from './operations/columns';
22
- import { createIndex, dropIndex } from './operations/indexes';
25
+ import { ifDefined } from '@prisma-next/utils/defined';
26
+ import { columnExistsAst, indexExistsAst, tableExistsAst } from '../../contract-free/checks';
27
+ import * as contractFreeDdl from '../../contract-free/ddl';
28
+ import { quoteIdentifier } from '../sql-utils';
29
+ import { addColumnExecuteSql, dropColumnExecuteSql } from './operations/columns';
23
30
  import type { SqliteColumnSpec, SqliteIndexSpec, SqliteTableSpec } from './operations/shared';
24
- import { createTable, dropTable, recreateTable } from './operations/tables';
31
+ import { step } from './operations/shared';
32
+ import { recreateTable } from './operations/tables';
33
+ import { buildCreateIndexSql, buildDropIndexSql } from './planner-ddl-builders';
25
34
  import type { SqlitePlanTargetDetails } from './planner-target-details';
35
+ import { buildTargetDetails } from './planner-target-details';
26
36
 
27
37
  type Op = SqlMigrationPlanOperation<SqlitePlanTargetDetails>;
28
38
 
@@ -32,7 +42,7 @@ abstract class SqliteOpFactoryCallNode extends TsExpression implements Framework
32
42
  abstract readonly factoryName: string;
33
43
  abstract readonly operationClass: MigrationOperationClass;
34
44
  abstract readonly label: string;
35
- abstract toOp(): Op;
45
+ abstract toOp(lowerer?: Lowerer): Op | Promise<Op>;
36
46
 
37
47
  importRequirements(): readonly ImportRequirement[] {
38
48
  return [{ moduleSpecifier: TARGET_MIGRATION_MODULE, symbol: this.factoryName }];
@@ -47,27 +57,149 @@ abstract class SqliteOpFactoryCallNode extends TsExpression implements Framework
47
57
  // Table
48
58
  // ============================================================================
49
59
 
60
+ // ---------------------------------------------------------------------------
61
+ // TypeScript rendering helpers for DdlColumn / DdlTableConstraint
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function renderDdlColumnDefault(def: AnyDdlColumnDefault | undefined): string {
65
+ if (!def) return '';
66
+ if (def.kind === 'literal') {
67
+ return `lit(${jsonToTsSource(def.value)})`;
68
+ }
69
+ return `fn(${jsonToTsSource(def.expression)})`;
70
+ }
71
+
72
+ function renderDdlColumnAsTsCall(column: DdlColumn): string {
73
+ const opts: string[] = [];
74
+ if (column.notNull) opts.push('notNull: true');
75
+ if (column.primaryKey) opts.push('primaryKey: true');
76
+ if (column.default) opts.push(`default: ${renderDdlColumnDefault(column.default)}`);
77
+ const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
78
+ return `col(${jsonToTsSource(column.name)}, ${jsonToTsSource(column.type)}${optsStr})`;
79
+ }
80
+
81
+ function renderDdlConstraintAsTsCall(constraint: DdlTableConstraint): string {
82
+ switch (constraint.kind) {
83
+ case 'primary-key': {
84
+ const nameOpt = constraint.name ? `, { name: ${jsonToTsSource(constraint.name)} }` : '';
85
+ return `primaryKey(${jsonToTsSource(constraint.columns)}${nameOpt})`;
86
+ }
87
+ case 'foreign-key': {
88
+ const opts: string[] = [];
89
+ if (constraint.name) opts.push(`name: ${jsonToTsSource(constraint.name)}`);
90
+ if (constraint.onDelete) opts.push(`onDelete: ${jsonToTsSource(constraint.onDelete)}`);
91
+ if (constraint.onUpdate) opts.push(`onUpdate: ${jsonToTsSource(constraint.onUpdate)}`);
92
+ const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
93
+ return `foreignKey(${jsonToTsSource(constraint.columns)}, ${jsonToTsSource(constraint.refTable)}, ${jsonToTsSource(constraint.refColumns)}${optsStr})`;
94
+ }
95
+ case 'unique': {
96
+ const nameOpt = constraint.name ? `, { name: ${jsonToTsSource(constraint.name)} }` : '';
97
+ return `unique(${jsonToTsSource(constraint.columns)}${nameOpt})`;
98
+ }
99
+ }
100
+ }
101
+
102
+ function constraintImportSymbols(constraints: readonly DdlTableConstraint[] | undefined): string[] {
103
+ if (!constraints || constraints.length === 0) return [];
104
+ const symbols = new Set<string>();
105
+ for (const c of constraints) {
106
+ if (c.kind === 'primary-key') symbols.add('primaryKey');
107
+ else if (c.kind === 'foreign-key') symbols.add('foreignKey');
108
+ else if (c.kind === 'unique') symbols.add('unique');
109
+ }
110
+ return [...symbols];
111
+ }
112
+
113
+ function defaultImportSymbols(columns: readonly DdlColumn[]): string[] {
114
+ const symbols = new Set<string>();
115
+ for (const col of columns) {
116
+ if (col.default?.kind === 'literal') symbols.add('lit');
117
+ else if (col.default?.kind === 'function') symbols.add('fn');
118
+ }
119
+ return [...symbols];
120
+ }
121
+
50
122
  export class CreateTableCall extends SqliteOpFactoryCallNode {
51
123
  readonly factoryName = 'createTable' as const;
52
124
  readonly operationClass = 'additive' as const;
53
125
  readonly tableName: string;
54
- readonly spec: SqliteTableSpec;
126
+ readonly columns: readonly DdlColumn[];
127
+ readonly constraints: readonly DdlTableConstraint[] | undefined;
55
128
  readonly label: string;
56
129
 
57
- constructor(tableName: string, spec: SqliteTableSpec) {
130
+ constructor(
131
+ tableName: string,
132
+ columns: readonly DdlColumn[],
133
+ constraints?: readonly DdlTableConstraint[],
134
+ ) {
58
135
  super();
59
136
  this.tableName = tableName;
60
- this.spec = spec;
137
+ this.columns = Object.freeze([...columns]);
138
+ this.constraints = constraints ? Object.freeze([...constraints]) : undefined;
61
139
  this.label = `Create table ${tableName}`;
62
140
  this.freeze();
63
141
  }
64
142
 
65
- toOp(): Op {
66
- return createTable(this.tableName, this.spec);
143
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
144
+ if (lowerer === undefined) {
145
+ throw new Error(
146
+ `CreateTableCall.toOp: a DDL lowerer is required on the SQLite planner path (table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
147
+ );
148
+ }
149
+ const ddlNode = contractFreeDdl.createTable({
150
+ table: this.tableName,
151
+ columns: this.columns,
152
+ ...ifDefined('constraints', this.constraints),
153
+ });
154
+ const statement = await lowerer.lowerToExecuteRequest(ddlNode);
155
+ const tableName = this.tableName;
156
+ const tableChecks = tableExistsAst(tableName);
157
+ const absent = await lowerer.lowerToExecuteRequest(tableChecks.tableAbsent());
158
+ const present = await lowerer.lowerToExecuteRequest(tableChecks.tablePresent());
159
+ return {
160
+ id: `table.${tableName}`,
161
+ label: `Create table ${tableName}`,
162
+ summary: `Creates table ${tableName} with required columns`,
163
+ operationClass: 'additive',
164
+ target: { id: 'sqlite', details: buildTargetDetails('table', tableName) },
165
+ precheck: [step(`ensure table "${tableName}" does not exist`, absent.sql, absent.params)],
166
+ execute: [
167
+ {
168
+ description: `create table "${tableName}"`,
169
+ sql: statement.sql,
170
+ params: statement.params ?? [],
171
+ },
172
+ ],
173
+ postcheck: [step(`verify table "${tableName}" exists`, present.sql, present.params)],
174
+ };
67
175
  }
68
176
 
69
177
  renderTypeScript(): string {
70
- return `createTable(${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.spec)})`;
178
+ const columnsList = this.columns.map(renderDdlColumnAsTsCall).join(', ');
179
+ const constraintsList = this.constraints
180
+ ? this.constraints.map(renderDdlConstraintAsTsCall).join(', ')
181
+ : undefined;
182
+
183
+ const opts: string[] = [];
184
+ opts.push(`table: ${jsonToTsSource(this.tableName)}`);
185
+ opts.push(`columns: [${columnsList}]`);
186
+ if (constraintsList) opts.push(`constraints: [${constraintsList}]`);
187
+
188
+ return `this.createTable({ ${opts.join(', ')} })`;
189
+ }
190
+
191
+ override importRequirements(): readonly ImportRequirement[] {
192
+ const req: ImportRequirement[] = [];
193
+ if (this.columns.length > 0) {
194
+ req.push({ moduleSpecifier: TARGET_MIGRATION_MODULE, symbol: 'col' });
195
+ for (const sym of defaultImportSymbols(this.columns)) {
196
+ req.push({ moduleSpecifier: TARGET_MIGRATION_MODULE, symbol: sym });
197
+ }
198
+ }
199
+ for (const sym of constraintImportSymbols(this.constraints)) {
200
+ req.push({ moduleSpecifier: TARGET_MIGRATION_MODULE, symbol: sym });
201
+ }
202
+ return req;
71
203
  }
72
204
  }
73
205
 
@@ -84,12 +216,35 @@ export class DropTableCall extends SqliteOpFactoryCallNode {
84
216
  this.freeze();
85
217
  }
86
218
 
87
- toOp(): Op {
88
- return dropTable(this.tableName);
219
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
220
+ if (lowerer === undefined) {
221
+ throw new Error(
222
+ `DropTableCall.toOp: a lowerer is required on the SQLite planner path (table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
223
+ );
224
+ }
225
+ const checks = tableExistsAst(this.tableName);
226
+ const present = await lowerer.lowerToExecuteRequest(checks.tablePresent());
227
+ const absent = await lowerer.lowerToExecuteRequest(checks.tableAbsent());
228
+ return {
229
+ id: `dropTable.${this.tableName}`,
230
+ label: `Drop table ${this.tableName}`,
231
+ summary: `Drops table ${this.tableName} which is not in the contract`,
232
+ operationClass: 'destructive',
233
+ target: { id: 'sqlite', details: buildTargetDetails('table', this.tableName) },
234
+ precheck: [step(`ensure table "${this.tableName}" exists`, present.sql, present.params)],
235
+ execute: [
236
+ step(`drop table "${this.tableName}"`, `DROP TABLE ${quoteIdentifier(this.tableName)}`),
237
+ ],
238
+ postcheck: [step(`verify table "${this.tableName}" is gone`, absent.sql, absent.params)],
239
+ };
89
240
  }
90
241
 
91
242
  renderTypeScript(): string {
92
- return `dropTable(${jsonToTsSource(this.tableName)})`;
243
+ return `this.dropTable({ table: ${jsonToTsSource(this.tableName)} })`;
244
+ }
245
+
246
+ override importRequirements(): readonly ImportRequirement[] {
247
+ return [];
93
248
  }
94
249
  }
95
250
 
@@ -125,16 +280,24 @@ export class RecreateTableCall extends SqliteOpFactoryCallNode {
125
280
  this.freeze();
126
281
  }
127
282
 
128
- toOp(): Op {
129
- return recreateTable({
130
- tableName: this.tableName,
131
- contractTable: this.contractTable,
132
- schemaColumnNames: this.schemaColumnNames,
133
- indexes: this.indexes,
134
- summary: this.summary,
135
- postchecks: this.postchecks,
136
- operationClass: this.operationClass,
137
- });
283
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
284
+ if (lowerer === undefined) {
285
+ throw new Error(
286
+ `RecreateTableCall.toOp: a lowerer is required on the SQLite planner path (table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
287
+ );
288
+ }
289
+ return recreateTable(
290
+ {
291
+ tableName: this.tableName,
292
+ contractTable: this.contractTable,
293
+ schemaColumnNames: this.schemaColumnNames,
294
+ indexes: this.indexes,
295
+ summary: this.summary,
296
+ postchecks: this.postchecks,
297
+ operationClass: this.operationClass,
298
+ },
299
+ lowerer,
300
+ );
138
301
  }
139
302
 
140
303
  renderTypeScript(): string {
@@ -147,7 +310,11 @@ export class RecreateTableCall extends SqliteOpFactoryCallNode {
147
310
  postchecks: this.postchecks,
148
311
  operationClass: this.operationClass,
149
312
  };
150
- return `recreateTable(${jsonToTsSource(args)})`;
313
+ return `this.recreateTable(${jsonToTsSource(args)})`;
314
+ }
315
+
316
+ override importRequirements(): readonly ImportRequirement[] {
317
+ return [];
151
318
  }
152
319
  }
153
320
 
@@ -172,12 +339,38 @@ export class AddColumnCall extends SqliteOpFactoryCallNode {
172
339
  this.freeze();
173
340
  }
174
341
 
175
- toOp(): Op {
176
- return addColumn(this.tableName, this.column);
342
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
343
+ if (lowerer === undefined) {
344
+ throw new Error(
345
+ `AddColumnCall.toOp: a lowerer is required on the SQLite planner path (column "${this.column.name}" on table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
346
+ );
347
+ }
348
+ const checks = columnExistsAst(this.tableName, this.column.name);
349
+ const absent = await lowerer.lowerToExecuteRequest(checks.columnAbsent());
350
+ const present = await lowerer.lowerToExecuteRequest(checks.columnPresent());
351
+ return {
352
+ id: `column.${this.tableName}.${this.column.name}`,
353
+ label: `Add column ${this.column.name} on ${this.tableName}`,
354
+ summary: `Adds column ${this.column.name} on ${this.tableName}`,
355
+ operationClass: 'additive',
356
+ target: {
357
+ id: 'sqlite',
358
+ details: buildTargetDetails('column', this.column.name, this.tableName),
359
+ },
360
+ precheck: [step(`ensure column "${this.column.name}" is missing`, absent.sql, absent.params)],
361
+ execute: [
362
+ step(`add column "${this.column.name}"`, addColumnExecuteSql(this.tableName, this.column)),
363
+ ],
364
+ postcheck: [step(`verify column "${this.column.name}" exists`, present.sql, present.params)],
365
+ };
177
366
  }
178
367
 
179
368
  renderTypeScript(): string {
180
- return `addColumn(${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.column)})`;
369
+ return `this.addColumn({ table: ${jsonToTsSource(this.tableName)}, column: ${jsonToTsSource(this.column)} })`;
370
+ }
371
+
372
+ override importRequirements(): readonly ImportRequirement[] {
373
+ return [];
181
374
  }
182
375
  }
183
376
 
@@ -196,12 +389,53 @@ export class DropColumnCall extends SqliteOpFactoryCallNode {
196
389
  this.freeze();
197
390
  }
198
391
 
199
- toOp(): Op {
200
- return dropColumn(this.tableName, this.columnName);
392
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
393
+ if (lowerer === undefined) {
394
+ throw new Error(
395
+ `DropColumnCall.toOp: a lowerer is required on the SQLite planner path (column "${this.columnName}" on table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
396
+ );
397
+ }
398
+ const checks = columnExistsAst(this.tableName, this.columnName);
399
+ const present = await lowerer.lowerToExecuteRequest(checks.columnPresent());
400
+ const absent = await lowerer.lowerToExecuteRequest(checks.columnAbsent());
401
+ return {
402
+ id: `dropColumn.${this.tableName}.${this.columnName}`,
403
+ label: `Drop column ${this.columnName} on ${this.tableName}`,
404
+ summary: `Drops column ${this.columnName} on ${this.tableName} which is not in the contract`,
405
+ operationClass: 'destructive',
406
+ target: {
407
+ id: 'sqlite',
408
+ details: buildTargetDetails('column', this.columnName, this.tableName),
409
+ },
410
+ precheck: [
411
+ step(
412
+ `ensure column "${this.columnName}" exists on "${this.tableName}"`,
413
+ present.sql,
414
+ present.params,
415
+ ),
416
+ ],
417
+ execute: [
418
+ step(
419
+ `drop column "${this.columnName}" from "${this.tableName}"`,
420
+ dropColumnExecuteSql(this.tableName, this.columnName),
421
+ ),
422
+ ],
423
+ postcheck: [
424
+ step(
425
+ `verify column "${this.columnName}" is gone from "${this.tableName}"`,
426
+ absent.sql,
427
+ absent.params,
428
+ ),
429
+ ],
430
+ };
201
431
  }
202
432
 
203
433
  renderTypeScript(): string {
204
- return `dropColumn(${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.columnName)})`;
434
+ return `this.dropColumn({ table: ${jsonToTsSource(this.tableName)}, column: ${jsonToTsSource(this.columnName)} })`;
435
+ }
436
+
437
+ override importRequirements(): readonly ImportRequirement[] {
438
+ return [];
205
439
  }
206
440
  }
207
441
 
@@ -226,12 +460,41 @@ export class CreateIndexCall extends SqliteOpFactoryCallNode {
226
460
  this.freeze();
227
461
  }
228
462
 
229
- toOp(): Op {
230
- return createIndex(this.tableName, this.indexName, this.columns);
463
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
464
+ if (lowerer === undefined) {
465
+ throw new Error(
466
+ `CreateIndexCall.toOp: a lowerer is required on the SQLite planner path (index "${this.indexName}" on table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
467
+ );
468
+ }
469
+ const checks = indexExistsAst(this.indexName);
470
+ const absent = await lowerer.lowerToExecuteRequest(checks.indexAbsent());
471
+ const present = await lowerer.lowerToExecuteRequest(checks.indexPresent());
472
+ return {
473
+ id: `index.${this.tableName}.${this.indexName}`,
474
+ label: `Create index ${this.indexName} on ${this.tableName}`,
475
+ summary: `Creates index ${this.indexName} on ${this.tableName}`,
476
+ operationClass: 'additive',
477
+ target: {
478
+ id: 'sqlite',
479
+ details: buildTargetDetails('index', this.indexName, this.tableName),
480
+ },
481
+ precheck: [step(`ensure index "${this.indexName}" is missing`, absent.sql, absent.params)],
482
+ execute: [
483
+ step(
484
+ `create index "${this.indexName}"`,
485
+ buildCreateIndexSql(this.tableName, this.indexName, this.columns),
486
+ ),
487
+ ],
488
+ postcheck: [step(`verify index "${this.indexName}" exists`, present.sql, present.params)],
489
+ };
231
490
  }
232
491
 
233
492
  renderTypeScript(): string {
234
- return `createIndex(${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.indexName)}, ${jsonToTsSource(this.columns)})`;
493
+ return `this.createIndex({ table: ${jsonToTsSource(this.tableName)}, index: ${jsonToTsSource(this.indexName)}, columns: ${jsonToTsSource(this.columns)} })`;
494
+ }
495
+
496
+ override importRequirements(): readonly ImportRequirement[] {
497
+ return [];
235
498
  }
236
499
  }
237
500
 
@@ -250,12 +513,36 @@ export class DropIndexCall extends SqliteOpFactoryCallNode {
250
513
  this.freeze();
251
514
  }
252
515
 
253
- toOp(): Op {
254
- return dropIndex(this.tableName, this.indexName);
516
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
517
+ if (lowerer === undefined) {
518
+ throw new Error(
519
+ `DropIndexCall.toOp: a lowerer is required on the SQLite planner path (index "${this.indexName}" on table "${this.tableName}"). Pass the control adapter to createSqliteMigrationPlanner.`,
520
+ );
521
+ }
522
+ const checks = indexExistsAst(this.indexName);
523
+ const present = await lowerer.lowerToExecuteRequest(checks.indexPresent());
524
+ const absent = await lowerer.lowerToExecuteRequest(checks.indexAbsent());
525
+ return {
526
+ id: `dropIndex.${this.tableName}.${this.indexName}`,
527
+ label: `Drop index ${this.indexName} on ${this.tableName}`,
528
+ summary: `Drops index ${this.indexName} on ${this.tableName} which is not in the contract`,
529
+ operationClass: 'destructive',
530
+ target: {
531
+ id: 'sqlite',
532
+ details: buildTargetDetails('index', this.indexName, this.tableName),
533
+ },
534
+ precheck: [step(`ensure index "${this.indexName}" exists`, present.sql, present.params)],
535
+ execute: [step(`drop index "${this.indexName}"`, buildDropIndexSql(this.indexName))],
536
+ postcheck: [step(`verify index "${this.indexName}" is gone`, absent.sql, absent.params)],
537
+ };
255
538
  }
256
539
 
257
540
  renderTypeScript(): string {
258
- return `dropIndex(${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.indexName)})`;
541
+ return `this.dropIndex({ table: ${jsonToTsSource(this.tableName)}, index: ${jsonToTsSource(this.indexName)} })`;
542
+ }
543
+
544
+ override importRequirements(): readonly ImportRequirement[] {
545
+ return [];
259
546
  }
260
547
  }
261
548
 
@@ -292,7 +579,7 @@ export class DataTransformCall extends SqliteOpFactoryCallNode {
292
579
  this.freeze();
293
580
  }
294
581
 
295
- toOp(): Op {
582
+ toOp(_lowerer?: Lowerer): Op {
296
583
  throw errorUnfilledPlaceholder(this.label);
297
584
  }
298
585
 
@@ -344,7 +631,7 @@ export class RawSqlCall extends SqliteOpFactoryCallNode {
344
631
  this.freeze();
345
632
  }
346
633
 
347
- toOp(): Op {
634
+ toOp(_lowerer?: Lowerer): Op {
348
635
  return this.op;
349
636
  }
350
637
 
@@ -1,39 +1,51 @@
1
- import { escapeLiteral, quoteIdentifier } from '../../sql-utils';
1
+ import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
+ import { columnExistsAst } from '../../../contract-free/checks';
3
+ import { quoteIdentifier } from '../../sql-utils';
2
4
  import { buildTargetDetails } from '../planner-target-details';
3
5
  import { type Op, type SqliteColumnSpec, step } from './shared';
4
6
 
5
- export function addColumn(tableName: string, column: SqliteColumnSpec): Op {
7
+ export function addColumnExecuteSql(tableName: string, column: SqliteColumnSpec): string {
6
8
  const parts = [
7
9
  `ALTER TABLE ${quoteIdentifier(tableName)}`,
8
10
  `ADD COLUMN ${quoteIdentifier(column.name)} ${column.typeSql}`,
9
11
  column.defaultSql,
10
12
  column.nullable ? '' : 'NOT NULL',
11
13
  ].filter(Boolean);
12
- const addSql = parts.join(' ');
14
+ return parts.join(' ');
15
+ }
16
+
17
+ export function dropColumnExecuteSql(tableName: string, columnName: string): string {
18
+ return `ALTER TABLE ${quoteIdentifier(tableName)} DROP COLUMN ${quoteIdentifier(columnName)}`;
19
+ }
13
20
 
21
+ export async function addColumn(
22
+ tableName: string,
23
+ column: SqliteColumnSpec,
24
+ lowerer: ExecuteRequestLowerer,
25
+ ): Promise<Op> {
26
+ const checks = columnExistsAst(tableName, column.name);
27
+ const absent = await lowerer.lowerToExecuteRequest(checks.columnAbsent());
28
+ const present = await lowerer.lowerToExecuteRequest(checks.columnPresent());
14
29
  return {
15
30
  id: `column.${tableName}.${column.name}`,
16
31
  label: `Add column ${column.name} on ${tableName}`,
17
32
  summary: `Adds column ${column.name} on ${tableName}`,
18
33
  operationClass: 'additive',
19
34
  target: { id: 'sqlite', details: buildTargetDetails('column', column.name, tableName) },
20
- precheck: [
21
- step(
22
- `ensure column "${column.name}" is missing`,
23
- `SELECT COUNT(*) = 0 FROM pragma_table_info('${escapeLiteral(tableName)}') WHERE name = '${escapeLiteral(column.name)}'`,
24
- ),
25
- ],
26
- execute: [step(`add column "${column.name}"`, addSql)],
27
- postcheck: [
28
- step(
29
- `verify column "${column.name}" exists`,
30
- `SELECT COUNT(*) > 0 FROM pragma_table_info('${escapeLiteral(tableName)}') WHERE name = '${escapeLiteral(column.name)}'`,
31
- ),
32
- ],
35
+ precheck: [step(`ensure column "${column.name}" is missing`, absent.sql, absent.params)],
36
+ execute: [step(`add column "${column.name}"`, addColumnExecuteSql(tableName, column))],
37
+ postcheck: [step(`verify column "${column.name}" exists`, present.sql, present.params)],
33
38
  };
34
39
  }
35
40
 
36
- export function dropColumn(tableName: string, columnName: string): Op {
41
+ export async function dropColumn(
42
+ tableName: string,
43
+ columnName: string,
44
+ lowerer: ExecuteRequestLowerer,
45
+ ): Promise<Op> {
46
+ const checks = columnExistsAst(tableName, columnName);
47
+ const present = await lowerer.lowerToExecuteRequest(checks.columnPresent());
48
+ const absent = await lowerer.lowerToExecuteRequest(checks.columnAbsent());
37
49
  return {
38
50
  id: `dropColumn.${tableName}.${columnName}`,
39
51
  label: `Drop column ${columnName} on ${tableName}`,
@@ -41,22 +53,16 @@ export function dropColumn(tableName: string, columnName: string): Op {
41
53
  operationClass: 'destructive',
42
54
  target: { id: 'sqlite', details: buildTargetDetails('column', columnName, tableName) },
43
55
  precheck: [
44
- step(
45
- `ensure column "${columnName}" exists on "${tableName}"`,
46
- `SELECT COUNT(*) > 0 FROM pragma_table_info('${escapeLiteral(tableName)}') WHERE name = '${escapeLiteral(columnName)}'`,
47
- ),
56
+ step(`ensure column "${columnName}" exists on "${tableName}"`, present.sql, present.params),
48
57
  ],
49
58
  execute: [
50
59
  step(
51
60
  `drop column "${columnName}" from "${tableName}"`,
52
- `ALTER TABLE ${quoteIdentifier(tableName)} DROP COLUMN ${quoteIdentifier(columnName)}`,
61
+ dropColumnExecuteSql(tableName, columnName),
53
62
  ),
54
63
  ],
55
64
  postcheck: [
56
- step(
57
- `verify column "${columnName}" is gone from "${tableName}"`,
58
- `SELECT COUNT(*) = 0 FROM pragma_table_info('${escapeLiteral(tableName)}') WHERE name = '${escapeLiteral(columnName)}'`,
59
- ),
65
+ step(`verify column "${columnName}" is gone from "${tableName}"`, absent.sql, absent.params),
60
66
  ],
61
67
  };
62
68
  }
@@ -1,52 +1,56 @@
1
- import { escapeLiteral } from '../../sql-utils';
1
+ import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
+ import { indexExistsAst } from '../../../contract-free/checks';
2
3
  import { buildCreateIndexSql, buildDropIndexSql } from '../planner-ddl-builders';
3
4
  import { buildTargetDetails } from '../planner-target-details';
4
5
  import { type Op, step } from './shared';
5
6
 
6
- export function createIndex(tableName: string, indexName: string, columns: readonly string[]): Op {
7
+ type CheckStep = { sql: string; params?: readonly unknown[] };
8
+
9
+ async function indexExistsSteps(
10
+ lowerer: ExecuteRequestLowerer,
11
+ indexName: string,
12
+ ): Promise<{ present: CheckStep; absent: CheckStep }> {
13
+ const checks = indexExistsAst(indexName);
14
+ const present = await lowerer.lowerToExecuteRequest(checks.indexPresent());
15
+ const absent = await lowerer.lowerToExecuteRequest(checks.indexAbsent());
16
+ return { present, absent };
17
+ }
18
+
19
+ export async function createIndex(
20
+ tableName: string,
21
+ indexName: string,
22
+ columns: readonly string[],
23
+ lowerer: ExecuteRequestLowerer,
24
+ ): Promise<Op> {
25
+ const { present, absent } = await indexExistsSteps(lowerer, indexName);
7
26
  return {
8
27
  id: `index.${tableName}.${indexName}`,
9
28
  label: `Create index ${indexName} on ${tableName}`,
10
29
  summary: `Creates index ${indexName} on ${tableName}`,
11
30
  operationClass: 'additive',
12
31
  target: { id: 'sqlite', details: buildTargetDetails('index', indexName, tableName) },
13
- precheck: [
14
- step(
15
- `ensure index "${indexName}" is missing`,
16
- `SELECT COUNT(*) = 0 FROM sqlite_master WHERE type = 'index' AND name = '${escapeLiteral(indexName)}'`,
17
- ),
18
- ],
32
+ precheck: [step(`ensure index "${indexName}" is missing`, absent.sql, absent.params)],
19
33
  execute: [
20
34
  step(`create index "${indexName}"`, buildCreateIndexSql(tableName, indexName, columns)),
21
35
  ],
22
- postcheck: [
23
- step(
24
- `verify index "${indexName}" exists`,
25
- `SELECT COUNT(*) > 0 FROM sqlite_master WHERE type = 'index' AND name = '${escapeLiteral(indexName)}'`,
26
- ),
27
- ],
36
+ postcheck: [step(`verify index "${indexName}" exists`, present.sql, present.params)],
28
37
  };
29
38
  }
30
39
 
31
- export function dropIndex(tableName: string, indexName: string): Op {
40
+ export async function dropIndex(
41
+ tableName: string,
42
+ indexName: string,
43
+ lowerer: ExecuteRequestLowerer,
44
+ ): Promise<Op> {
45
+ const { present, absent } = await indexExistsSteps(lowerer, indexName);
32
46
  return {
33
47
  id: `dropIndex.${tableName}.${indexName}`,
34
48
  label: `Drop index ${indexName} on ${tableName}`,
35
49
  summary: `Drops index ${indexName} on ${tableName} which is not in the contract`,
36
50
  operationClass: 'destructive',
37
51
  target: { id: 'sqlite', details: buildTargetDetails('index', indexName, tableName) },
38
- precheck: [
39
- step(
40
- `ensure index "${indexName}" exists`,
41
- `SELECT COUNT(*) > 0 FROM sqlite_master WHERE type = 'index' AND name = '${escapeLiteral(indexName)}'`,
42
- ),
43
- ],
52
+ precheck: [step(`ensure index "${indexName}" exists`, present.sql, present.params)],
44
53
  execute: [step(`drop index "${indexName}"`, buildDropIndexSql(indexName))],
45
- postcheck: [
46
- step(
47
- `verify index "${indexName}" is gone`,
48
- `SELECT COUNT(*) = 0 FROM sqlite_master WHERE type = 'index' AND name = '${escapeLiteral(indexName)}'`,
49
- ),
50
- ],
54
+ postcheck: [step(`verify index "${indexName}" is gone`, absent.sql, absent.params)],
51
55
  };
52
56
  }