zenstack-kit 0.1.2 → 0.1.4

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.
@@ -41,6 +41,7 @@ export declare class CommandError extends Error {
41
41
  */
42
42
  export declare function resolveConfig(ctx: CommandContext): Promise<{
43
43
  config: ZenStackKitConfig;
44
+ configDir: string;
44
45
  schemaPath: string;
45
46
  outputPath: string;
46
47
  dialect: "sqlite" | "postgres" | "mysql";
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH,OAAO,KAAK,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE9F,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,cAAc,CAAC;IACxB,GAAG,EAAE,KAAK,CAAC;IACX,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IAC9D,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,UAAU,GAAG,gBAAgB,CAAC,CAAC;IAC/D,iBAAiB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACxE,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACxF,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,sBAAsB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACrF;AAED,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;CAC1C,CAAC,CAkBD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAI7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GACvC,MAAM,GAAG,SAAS,CAKpB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAiFxE;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA0FhE;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAiEhE"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
@@ -30,7 +30,7 @@ export async function resolveConfig(ctx) {
30
30
  // Resolve paths relative to config file location
31
31
  const schemaPath = path.resolve(configDir, relativeSchemaPath);
32
32
  const outputPath = path.resolve(configDir, relativeOutputPath);
33
- return { config, schemaPath, outputPath, dialect };
33
+ return { config, configDir, schemaPath, outputPath, dialect };
34
34
  }
35
35
  /**
36
36
  * Validate that the schema file exists (schemaPath should be absolute)
@@ -177,6 +177,23 @@ export async function runMigrateApply(ctx) {
177
177
  migrationsTable,
178
178
  migrationsSchema,
179
179
  });
180
+ // Handle coherence errors
181
+ if (result.coherenceErrors && result.coherenceErrors.length > 0) {
182
+ ctx.log("error", "Migration history is inconsistent");
183
+ ctx.log("error", "");
184
+ ctx.log("error", "The following issues were found:");
185
+ for (const err of result.coherenceErrors) {
186
+ ctx.log("error", ` - ${err.details}`);
187
+ }
188
+ ctx.log("error", "");
189
+ ctx.log("error", "This can happen when:");
190
+ ctx.log("error", " - Migrations were applied manually without using zenstack-kit");
191
+ ctx.log("error", " - The migration log file was modified or deleted");
192
+ ctx.log("error", " - Different migration histories exist across environments");
193
+ ctx.log("error", "");
194
+ ctx.log("error", "To resolve, ensure your migration log matches your database state.");
195
+ throw new CommandError("Migration history is inconsistent");
196
+ }
180
197
  if (result.applied.length === 0 && !result.failed) {
181
198
  ctx.log("warning", "No pending migrations");
182
199
  if (result.alreadyApplied.length > 0) {
@@ -273,13 +290,14 @@ export async function runInit(ctx) {
273
290
  * pull command
274
291
  */
275
292
  export async function runPull(ctx) {
276
- const { config, dialect, outputPath: migrationsPath } = await resolveConfig(ctx);
293
+ const { config, configDir, dialect, outputPath: migrationsPath } = await resolveConfig(ctx);
277
294
  const connectionUrl = getConnectionUrl(config, dialect);
278
295
  if (dialect !== "sqlite" && !connectionUrl) {
279
296
  throw new CommandError("Database connection URL is required for non-sqlite dialects.");
280
297
  }
281
298
  const databasePath = dialect === "sqlite" ? connectionUrl : undefined;
282
- const schemaOutputPath = ctx.options.output || config.schema || "./schema.zmodel";
299
+ const relativeSchemaOutputPath = ctx.options.output || config.schema || "./schema.zmodel";
300
+ const schemaOutputPath = path.resolve(configDir, relativeSchemaOutputPath);
283
301
  // Check for existing files that would be affected
284
302
  const existingFiles = [];
285
303
  if (fs.existsSync(schemaOutputPath)) {
@@ -63,6 +63,16 @@ export interface ApplyPrismaMigrationsResult {
63
63
  migrationName: string;
64
64
  error: string;
65
65
  };
66
+ coherenceErrors?: MigrationCoherenceError[];
67
+ }
68
+ export interface MigrationCoherenceError {
69
+ type: "missing_from_log" | "missing_from_db" | "missing_from_disk" | "order_mismatch" | "checksum_mismatch";
70
+ migrationName: string;
71
+ details: string;
72
+ }
73
+ export interface MigrationCoherenceResult {
74
+ isCoherent: boolean;
75
+ errors: MigrationCoherenceError[];
66
76
  }
67
77
  export interface PreviewPrismaMigrationsResult {
68
78
  pending: Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/migrations/prisma.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/migrations/prisma.ts"],"names":[],"mappings":"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"}
@@ -67,6 +67,104 @@ export async function writeSnapshot(snapshotPath, schema) {
67
67
  /**
68
68
  * Diff two schemas and return the changes
69
69
  */
70
+ function diffTableChanges(previousModel, currentModel, tableName) {
71
+ const addedFields = [];
72
+ const removedFields = [];
73
+ const alteredFields = [];
74
+ const addedUniqueConstraints = [];
75
+ const removedUniqueConstraints = [];
76
+ const addedIndexes = [];
77
+ const removedIndexes = [];
78
+ const addedForeignKeys = [];
79
+ const removedForeignKeys = [];
80
+ const primaryKeyChanges = [];
81
+ const previousFields = new Map(previousModel.columns.map((f) => [f.name, f]));
82
+ const currentFields = new Map(currentModel.columns.map((f) => [f.name, f]));
83
+ for (const [columnName, column] of currentFields.entries()) {
84
+ if (!previousFields.has(columnName)) {
85
+ addedFields.push({ tableName, column });
86
+ }
87
+ }
88
+ for (const [columnName, column] of previousFields.entries()) {
89
+ if (!currentFields.has(columnName)) {
90
+ removedFields.push({ tableName, column });
91
+ }
92
+ }
93
+ for (const [columnName, currentColumn] of currentFields.entries()) {
94
+ const previousColumn = previousFields.get(columnName);
95
+ if (!previousColumn)
96
+ continue;
97
+ if (previousColumn.type !== currentColumn.type ||
98
+ previousColumn.notNull !== currentColumn.notNull ||
99
+ previousColumn.default !== currentColumn.default) {
100
+ alteredFields.push({
101
+ tableName,
102
+ columnName,
103
+ previous: previousColumn,
104
+ current: currentColumn,
105
+ });
106
+ }
107
+ }
108
+ const prevUnique = new Map(previousModel.uniqueConstraints.map((c) => [c.name, c]));
109
+ const currUnique = new Map(currentModel.uniqueConstraints.map((c) => [c.name, c]));
110
+ for (const [name, constraint] of currUnique.entries()) {
111
+ if (!prevUnique.has(name)) {
112
+ addedUniqueConstraints.push({ tableName, constraint });
113
+ }
114
+ }
115
+ for (const [name, constraint] of prevUnique.entries()) {
116
+ if (!currUnique.has(name)) {
117
+ removedUniqueConstraints.push({ tableName, constraint });
118
+ }
119
+ }
120
+ const prevIndexes = new Map(previousModel.indexes.map((i) => [i.name, i]));
121
+ const currIndexes = new Map(currentModel.indexes.map((i) => [i.name, i]));
122
+ for (const [name, index] of currIndexes.entries()) {
123
+ if (!prevIndexes.has(name)) {
124
+ addedIndexes.push({ tableName, index });
125
+ }
126
+ }
127
+ for (const [name, index] of prevIndexes.entries()) {
128
+ if (!currIndexes.has(name)) {
129
+ removedIndexes.push({ tableName, index });
130
+ }
131
+ }
132
+ const prevFks = new Map(previousModel.foreignKeys.map((f) => [f.name, f]));
133
+ const currFks = new Map(currentModel.foreignKeys.map((f) => [f.name, f]));
134
+ for (const [name, fk] of currFks.entries()) {
135
+ if (!prevFks.has(name)) {
136
+ addedForeignKeys.push({ tableName, foreignKey: fk });
137
+ }
138
+ }
139
+ for (const [name, fk] of prevFks.entries()) {
140
+ if (!currFks.has(name)) {
141
+ removedForeignKeys.push({ tableName, foreignKey: fk });
142
+ }
143
+ }
144
+ const prevPk = previousModel.primaryKey;
145
+ const currPk = currentModel.primaryKey;
146
+ const pkEqual = (prevPk?.name ?? "") === (currPk?.name ?? "") &&
147
+ JSON.stringify(prevPk?.columns ?? []) === JSON.stringify(currPk?.columns ?? []);
148
+ if (!pkEqual) {
149
+ primaryKeyChanges.push({
150
+ tableName,
151
+ previous: prevPk,
152
+ current: currPk,
153
+ });
154
+ }
155
+ return {
156
+ addedFields,
157
+ removedFields,
158
+ alteredFields,
159
+ addedUniqueConstraints,
160
+ removedUniqueConstraints,
161
+ addedIndexes,
162
+ removedIndexes,
163
+ addedForeignKeys,
164
+ removedForeignKeys,
165
+ primaryKeyChanges,
166
+ };
167
+ }
70
168
  function diffSchemas(previous, current) {
71
169
  const previousModels = new Map();
72
170
  const currentModels = new Map();
@@ -100,85 +198,17 @@ function diffSchemas(previous, current) {
100
198
  const previousModel = previousModels.get(tableName);
101
199
  if (!previousModel)
102
200
  continue;
103
- // Field changes
104
- const previousFields = new Map(previousModel.columns.map((f) => [f.name, f]));
105
- const currentFields = new Map(currentModel.columns.map((f) => [f.name, f]));
106
- for (const [columnName, column] of currentFields.entries()) {
107
- if (!previousFields.has(columnName)) {
108
- addedFields.push({ tableName, column });
109
- }
110
- }
111
- for (const [columnName, column] of previousFields.entries()) {
112
- if (!currentFields.has(columnName)) {
113
- removedFields.push({ tableName, column });
114
- }
115
- }
116
- for (const [columnName, currentColumn] of currentFields.entries()) {
117
- const previousColumn = previousFields.get(columnName);
118
- if (!previousColumn)
119
- continue;
120
- if (previousColumn.type !== currentColumn.type ||
121
- previousColumn.notNull !== currentColumn.notNull ||
122
- previousColumn.default !== currentColumn.default) {
123
- alteredFields.push({
124
- tableName,
125
- columnName,
126
- previous: previousColumn,
127
- current: currentColumn,
128
- });
129
- }
130
- }
131
- // Unique constraint changes
132
- const prevUnique = new Map(previousModel.uniqueConstraints.map((c) => [c.name, c]));
133
- const currUnique = new Map(currentModel.uniqueConstraints.map((c) => [c.name, c]));
134
- for (const [name, constraint] of currUnique.entries()) {
135
- if (!prevUnique.has(name)) {
136
- addedUniqueConstraints.push({ tableName, constraint });
137
- }
138
- }
139
- for (const [name, constraint] of prevUnique.entries()) {
140
- if (!currUnique.has(name)) {
141
- removedUniqueConstraints.push({ tableName, constraint });
142
- }
143
- }
144
- // Index changes
145
- const prevIndexes = new Map(previousModel.indexes.map((i) => [i.name, i]));
146
- const currIndexes = new Map(currentModel.indexes.map((i) => [i.name, i]));
147
- for (const [name, index] of currIndexes.entries()) {
148
- if (!prevIndexes.has(name)) {
149
- addedIndexes.push({ tableName, index });
150
- }
151
- }
152
- for (const [name, index] of prevIndexes.entries()) {
153
- if (!currIndexes.has(name)) {
154
- removedIndexes.push({ tableName, index });
155
- }
156
- }
157
- // Foreign key changes
158
- const prevFks = new Map(previousModel.foreignKeys.map((f) => [f.name, f]));
159
- const currFks = new Map(currentModel.foreignKeys.map((f) => [f.name, f]));
160
- for (const [name, fk] of currFks.entries()) {
161
- if (!prevFks.has(name)) {
162
- addedForeignKeys.push({ tableName, foreignKey: fk });
163
- }
164
- }
165
- for (const [name, fk] of prevFks.entries()) {
166
- if (!currFks.has(name)) {
167
- removedForeignKeys.push({ tableName, foreignKey: fk });
168
- }
169
- }
170
- // Primary key changes
171
- const prevPk = previousModel.primaryKey;
172
- const currPk = currentModel.primaryKey;
173
- const pkEqual = (prevPk?.name ?? "") === (currPk?.name ?? "") &&
174
- JSON.stringify(prevPk?.columns ?? []) === JSON.stringify(currPk?.columns ?? []);
175
- if (!pkEqual) {
176
- primaryKeyChanges.push({
177
- tableName,
178
- previous: prevPk,
179
- current: currPk,
180
- });
181
- }
201
+ const modelDiff = diffTableChanges(previousModel, currentModel, tableName);
202
+ addedFields.push(...modelDiff.addedFields);
203
+ removedFields.push(...modelDiff.removedFields);
204
+ alteredFields.push(...modelDiff.alteredFields);
205
+ addedUniqueConstraints.push(...modelDiff.addedUniqueConstraints);
206
+ removedUniqueConstraints.push(...modelDiff.removedUniqueConstraints);
207
+ addedIndexes.push(...modelDiff.addedIndexes);
208
+ removedIndexes.push(...modelDiff.removedIndexes);
209
+ addedForeignKeys.push(...modelDiff.addedForeignKeys);
210
+ removedForeignKeys.push(...modelDiff.removedForeignKeys);
211
+ primaryKeyChanges.push(...modelDiff.primaryKeyChanges);
182
212
  }
183
213
  return {
184
214
  addedModels,
@@ -197,6 +227,136 @@ function diffSchemas(previous, current) {
197
227
  renamedColumns: [],
198
228
  };
199
229
  }
230
+ function columnsSignature(columns) {
231
+ return columns.join("|");
232
+ }
233
+ function consumeSignature(map, signature) {
234
+ const count = map.get(signature) ?? 0;
235
+ if (count > 0) {
236
+ map.set(signature, count - 1);
237
+ return true;
238
+ }
239
+ return false;
240
+ }
241
+ function buildSignatureCount(items, getSignature) {
242
+ const counts = new Map();
243
+ for (const item of items) {
244
+ const signature = getSignature(item);
245
+ counts.set(signature, (counts.get(signature) ?? 0) + 1);
246
+ }
247
+ return counts;
248
+ }
249
+ function columnsEqual(a, b) {
250
+ return JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
251
+ }
252
+ function filterRenamedConstraintChanges(previousModel, currentModel, modelDiff) {
253
+ const prevUnique = buildSignatureCount(previousModel.uniqueConstraints, (c) => columnsSignature(c.columns));
254
+ const currUnique = buildSignatureCount(currentModel.uniqueConstraints, (c) => columnsSignature(c.columns));
255
+ const prevIndexes = buildSignatureCount(previousModel.indexes, (i) => columnsSignature(i.columns));
256
+ const currIndexes = buildSignatureCount(currentModel.indexes, (i) => columnsSignature(i.columns));
257
+ const prevFks = buildSignatureCount(previousModel.foreignKeys, (f) => `${columnsSignature(f.columns)}->${f.referencedTable}:${columnsSignature(f.referencedColumns)}`);
258
+ const currFks = buildSignatureCount(currentModel.foreignKeys, (f) => `${columnsSignature(f.columns)}->${f.referencedTable}:${columnsSignature(f.referencedColumns)}`);
259
+ const addedUniqueConstraints = modelDiff.addedUniqueConstraints.filter(({ constraint }) => !consumeSignature(prevUnique, columnsSignature(constraint.columns)));
260
+ const removedUniqueConstraints = modelDiff.removedUniqueConstraints.filter(({ constraint }) => !consumeSignature(currUnique, columnsSignature(constraint.columns)));
261
+ const addedIndexes = modelDiff.addedIndexes.filter(({ index }) => !consumeSignature(prevIndexes, columnsSignature(index.columns)));
262
+ const removedIndexes = modelDiff.removedIndexes.filter(({ index }) => !consumeSignature(currIndexes, columnsSignature(index.columns)));
263
+ const addedForeignKeys = modelDiff.addedForeignKeys.filter(({ foreignKey }) => !consumeSignature(prevFks, `${columnsSignature(foreignKey.columns)}->${foreignKey.referencedTable}:${columnsSignature(foreignKey.referencedColumns)}`));
264
+ const removedForeignKeys = modelDiff.removedForeignKeys.filter(({ foreignKey }) => !consumeSignature(currFks, `${columnsSignature(foreignKey.columns)}->${foreignKey.referencedTable}:${columnsSignature(foreignKey.referencedColumns)}`));
265
+ let primaryKeyChanges = modelDiff.primaryKeyChanges;
266
+ if (previousModel.primaryKey && currentModel.primaryKey) {
267
+ if (columnsEqual(previousModel.primaryKey.columns, currentModel.primaryKey.columns)) {
268
+ primaryKeyChanges = [];
269
+ }
270
+ }
271
+ return {
272
+ ...modelDiff,
273
+ addedUniqueConstraints,
274
+ removedUniqueConstraints,
275
+ addedIndexes,
276
+ removedIndexes,
277
+ addedForeignKeys,
278
+ removedForeignKeys,
279
+ primaryKeyChanges,
280
+ };
281
+ }
282
+ function applyRenameMappings(diff, renameTables = [], renameColumns = []) {
283
+ const removedModels = [...diff.removedModels];
284
+ const addedModels = [...diff.addedModels];
285
+ const removedFields = [...diff.removedFields];
286
+ const addedFields = [...diff.addedFields];
287
+ const alteredFields = [...diff.alteredFields];
288
+ const addedUniqueConstraints = [...diff.addedUniqueConstraints];
289
+ const removedUniqueConstraints = [...diff.removedUniqueConstraints];
290
+ const addedIndexes = [...diff.addedIndexes];
291
+ const removedIndexes = [...diff.removedIndexes];
292
+ const addedForeignKeys = [...diff.addedForeignKeys];
293
+ const removedForeignKeys = [...diff.removedForeignKeys];
294
+ const primaryKeyChanges = [...diff.primaryKeyChanges];
295
+ const renamedTables = [];
296
+ const renamedColumns = [];
297
+ const renamedTableMap = new Map();
298
+ renameTables.forEach((mapping) => {
299
+ const fromIndex = removedModels.findIndex((model) => model.name === mapping.from);
300
+ const toIndex = addedModels.findIndex((model) => model.name === mapping.to);
301
+ if (fromIndex === -1 || toIndex === -1) {
302
+ return;
303
+ }
304
+ const previousModel = removedModels[fromIndex];
305
+ const currentModel = addedModels[toIndex];
306
+ removedModels.splice(fromIndex, 1);
307
+ addedModels.splice(toIndex, 1);
308
+ renamedTables.push({ from: mapping.from, to: mapping.to });
309
+ renamedTableMap.set(mapping.from, mapping.to);
310
+ const modelDiff = filterRenamedConstraintChanges(previousModel, currentModel, diffTableChanges(previousModel, currentModel, mapping.to));
311
+ addedFields.push(...modelDiff.addedFields);
312
+ removedFields.push(...modelDiff.removedFields);
313
+ alteredFields.push(...modelDiff.alteredFields);
314
+ addedUniqueConstraints.push(...modelDiff.addedUniqueConstraints);
315
+ removedUniqueConstraints.push(...modelDiff.removedUniqueConstraints);
316
+ addedIndexes.push(...modelDiff.addedIndexes);
317
+ removedIndexes.push(...modelDiff.removedIndexes);
318
+ addedForeignKeys.push(...modelDiff.addedForeignKeys);
319
+ removedForeignKeys.push(...modelDiff.removedForeignKeys);
320
+ primaryKeyChanges.push(...modelDiff.primaryKeyChanges);
321
+ });
322
+ const remapTableName = (tableName) => renamedTableMap.get(tableName) ?? tableName;
323
+ const remapTableEntries = (items) => items.map((item) => ({ ...item, tableName: remapTableName(item.tableName) }));
324
+ if (renamedTableMap.size > 0) {
325
+ removedFields.forEach((entry) => {
326
+ const mapped = renamedTableMap.get(entry.tableName);
327
+ if (mapped) {
328
+ entry.tableName = mapped;
329
+ }
330
+ });
331
+ }
332
+ renameColumns.forEach((mapping) => {
333
+ const mappedTable = remapTableName(mapping.table);
334
+ const removedIdx = removedFields.findIndex((f) => f.tableName === mappedTable && f.column.name === mapping.from);
335
+ const addedIdx = addedFields.findIndex((f) => f.tableName === mappedTable && f.column.name === mapping.to);
336
+ if (removedIdx !== -1 && addedIdx !== -1) {
337
+ removedFields.splice(removedIdx, 1);
338
+ addedFields.splice(addedIdx, 1);
339
+ renamedColumns.push({ tableName: mappedTable, from: mapping.from, to: mapping.to });
340
+ }
341
+ });
342
+ return {
343
+ ...diff,
344
+ removedModels,
345
+ addedModels,
346
+ removedFields: remapTableEntries(removedFields),
347
+ addedFields: remapTableEntries(addedFields),
348
+ alteredFields: remapTableEntries(alteredFields),
349
+ renamedTables,
350
+ renamedColumns,
351
+ addedUniqueConstraints: remapTableEntries(addedUniqueConstraints),
352
+ removedUniqueConstraints: remapTableEntries(removedUniqueConstraints),
353
+ addedIndexes: remapTableEntries(addedIndexes),
354
+ removedIndexes: remapTableEntries(removedIndexes),
355
+ addedForeignKeys: remapTableEntries(addedForeignKeys),
356
+ removedForeignKeys: remapTableEntries(removedForeignKeys),
357
+ primaryKeyChanges: remapTableEntries(primaryKeyChanges),
358
+ };
359
+ }
200
360
  /**
201
361
  * Topologically sort tables so that referenced tables come before tables that reference them.
202
362
  * Tables with no foreign keys come first, then tables that only reference already-ordered tables.
@@ -357,30 +517,7 @@ export async function createPrismaMigration(options) {
357
517
  const currentSchema = await generateSchemaSnapshot(options.schemaPath);
358
518
  const { snapshotPath } = getSnapshotPaths(options.outputPath);
359
519
  const previousSnapshot = await readSnapshot(snapshotPath);
360
- let diff = diffSchemas(previousSnapshot?.schema ?? null, currentSchema);
361
- // Apply rename mappings
362
- if (options.renameTables?.length || options.renameColumns?.length) {
363
- // Handle table renames
364
- for (const mapping of options.renameTables ?? []) {
365
- const removedIdx = diff.removedModels.findIndex((m) => m.name === mapping.from);
366
- const addedIdx = diff.addedModels.findIndex((m) => m.name === mapping.to);
367
- if (removedIdx !== -1 && addedIdx !== -1) {
368
- diff.removedModels.splice(removedIdx, 1);
369
- diff.addedModels.splice(addedIdx, 1);
370
- diff.renamedTables.push(mapping);
371
- }
372
- }
373
- // Handle column renames
374
- for (const mapping of options.renameColumns ?? []) {
375
- const removedIdx = diff.removedFields.findIndex((f) => f.tableName === mapping.table && f.column.name === mapping.from);
376
- const addedIdx = diff.addedFields.findIndex((f) => f.tableName === mapping.table && f.column.name === mapping.to);
377
- if (removedIdx !== -1 && addedIdx !== -1) {
378
- diff.removedFields.splice(removedIdx, 1);
379
- diff.addedFields.splice(addedIdx, 1);
380
- diff.renamedColumns.push({ tableName: mapping.table, from: mapping.from, to: mapping.to });
381
- }
382
- }
383
- }
520
+ const diff = applyRenameMappings(diffSchemas(previousSnapshot?.schema ?? null, currentSchema), options.renameTables, options.renameColumns);
384
521
  const { up, down } = buildSqlStatements(diff, options.dialect);
385
522
  if (up.length === 0) {
386
523
  return null;
@@ -554,6 +691,79 @@ async function recordMigration(db, tableName, schema, dialect, migrationName, ch
554
691
  export function calculateChecksum(sql) {
555
692
  return crypto.createHash("sha256").update(sql).digest("hex");
556
693
  }
694
+ /**
695
+ * Validate that the database's applied migrations are coherent with the migration log.
696
+ *
697
+ * Coherence rules:
698
+ * 1. Every migration applied in the DB must exist in the migration log
699
+ * 2. Applied migrations must be a prefix of the log (no gaps)
700
+ * 3. Checksums must match for applied migrations
701
+ */
702
+ function validateMigrationCoherence(appliedMigrations, migrationLog, migrationFolders) {
703
+ const errors = [];
704
+ // Build a set of log migration names for quick lookup
705
+ const logMigrationNames = new Set(migrationLog.map((e) => e.name));
706
+ const folderNames = new Set(migrationFolders);
707
+ for (const entry of migrationLog) {
708
+ if (!folderNames.has(entry.name)) {
709
+ errors.push({
710
+ type: "missing_from_disk",
711
+ migrationName: entry.name,
712
+ details: `Migration "${entry.name}" exists in migration log but not on disk`,
713
+ });
714
+ }
715
+ }
716
+ // Check 1: Every applied migration must exist in the log
717
+ for (const [migrationName, row] of appliedMigrations) {
718
+ if (!logMigrationNames.has(migrationName)) {
719
+ errors.push({
720
+ type: "missing_from_log",
721
+ migrationName,
722
+ details: `Migration "${migrationName}" exists in database but not in migration log`,
723
+ });
724
+ }
725
+ }
726
+ // If there are migrations missing from the log, return early
727
+ // (other checks don't make sense if the log is incomplete)
728
+ if (errors.length > 0) {
729
+ return { isCoherent: false, errors };
730
+ }
731
+ // Check 2: Applied migrations should be a continuous prefix of the log
732
+ // i.e., if migration N is applied, all migrations before N in the log must also be applied
733
+ let lastAppliedIndex = -1;
734
+ for (let i = 0; i < migrationLog.length; i++) {
735
+ const logEntry = migrationLog[i];
736
+ const isApplied = appliedMigrations.has(logEntry.name);
737
+ if (isApplied) {
738
+ // Check for gaps: if this is applied, all previous should be applied
739
+ if (lastAppliedIndex !== i - 1) {
740
+ // There's a gap - find the missing migrations
741
+ for (let j = lastAppliedIndex + 1; j < i; j++) {
742
+ const missing = migrationLog[j];
743
+ errors.push({
744
+ type: "order_mismatch",
745
+ migrationName: missing.name,
746
+ details: `Migration "${missing.name}" is in the log but not applied, yet later migration "${logEntry.name}" is applied`,
747
+ });
748
+ }
749
+ }
750
+ lastAppliedIndex = i;
751
+ // Check 3: Checksum validation for applied migrations
752
+ const dbRow = appliedMigrations.get(logEntry.name);
753
+ if (dbRow.checksum !== logEntry.checksum) {
754
+ errors.push({
755
+ type: "checksum_mismatch",
756
+ migrationName: logEntry.name,
757
+ details: `Checksum mismatch for "${logEntry.name}": database has ${dbRow.checksum.slice(0, 8)}..., log has ${logEntry.checksum.slice(0, 8)}...`,
758
+ });
759
+ }
760
+ }
761
+ }
762
+ return {
763
+ isCoherent: errors.length === 0,
764
+ errors,
765
+ };
766
+ }
557
767
  /**
558
768
  * Execute raw SQL using the database driver directly
559
769
  * This bypasses Kysely for DDL statements which don't work reliably with sql.raw()
@@ -573,10 +783,19 @@ async function executeRawSql(dialect, sqlContent, options) {
573
783
  else if (dialect === "postgres") {
574
784
  const { Pool } = await import("pg");
575
785
  const pool = new Pool({ connectionString: options.connectionUrl });
786
+ const client = await pool.connect();
576
787
  try {
577
- await pool.query(sqlContent);
788
+ // PostgreSQL supports transactional DDL, so wrap migration in a transaction
789
+ await client.query("BEGIN");
790
+ await client.query(sqlContent);
791
+ await client.query("COMMIT");
792
+ }
793
+ catch (error) {
794
+ await client.query("ROLLBACK");
795
+ throw error;
578
796
  }
579
797
  finally {
798
+ client.release();
580
799
  await pool.end();
581
800
  }
582
801
  }
@@ -622,11 +841,32 @@ export async function applyPrismaMigrations(options) {
622
841
  .filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
623
842
  .map((e) => e.name)
624
843
  .sort();
844
+ const migrationFoldersWithSql = [];
845
+ for (const folderName of migrationFolders) {
846
+ const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
847
+ try {
848
+ await fs.access(sqlPath);
849
+ migrationFoldersWithSql.push(folderName);
850
+ }
851
+ catch {
852
+ // Missing migration.sql; coherence check will flag if it's in the log
853
+ }
854
+ }
855
+ // Read migration log and validate coherence
856
+ const migrationLog = await readMigrationLog(options.migrationsFolder);
857
+ const coherence = validateMigrationCoherence(appliedMigrations, migrationLog, migrationFoldersWithSql);
858
+ if (!coherence.isCoherent) {
859
+ return {
860
+ applied: [],
861
+ alreadyApplied: [],
862
+ coherenceErrors: coherence.errors,
863
+ };
864
+ }
625
865
  const result = {
626
866
  applied: [],
627
867
  alreadyApplied: [],
628
868
  };
629
- for (const folderName of migrationFolders) {
869
+ for (const folderName of migrationFoldersWithSql) {
630
870
  if (appliedMigrations.has(folderName)) {
631
871
  result.alreadyApplied.push(folderName);
632
872
  continue;
@@ -640,8 +880,7 @@ export async function applyPrismaMigrations(options) {
640
880
  continue; // Skip if no migration.sql
641
881
  }
642
882
  const checksum = calculateChecksum(sqlContent);
643
- // Verify checksum against migration log
644
- const migrationLog = await readMigrationLog(options.migrationsFolder);
883
+ // Verify checksum against migration log (migrationLog already read above)
645
884
  const logEntry = migrationLog.find((m) => m.name === folderName);
646
885
  if (logEntry && logEntry.checksum !== checksum) {
647
886
  result.failed = {
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/schema/pull.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEnF,MAAM,WAAW,WAAW;IAC1B,uBAAuB;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAmfD,wBAAsB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAwC1E"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/schema/pull.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEnF,MAAM,WAAW,WAAW;IAC1B,uBAAuB;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAofD,wBAAsB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAwC1E"}
@@ -73,9 +73,10 @@ function normalizeType(dataType) {
73
73
  return { type: "String", isArray };
74
74
  }
75
75
  function buildDatasourceBlock(dialect) {
76
+ const provider = dialect === "postgres" ? "postgresql" : dialect;
76
77
  return [
77
78
  "datasource db {",
78
- ` provider = \"${dialect}\"`,
79
+ ` provider = \"${provider}\"`,
79
80
  " url = env(\"DATABASE_URL\")",
80
81
  "}",
81
82
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenstack-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Drizzle-kit like CLI tooling for ZenStack schemas with Kysely support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",