zenstack-kit 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,7 @@ Options:
124
124
  - `-m, --migrations <path>` - Migrations directory
125
125
  - `--baseline` - Create snapshot only, no migration
126
126
  - `--create-initial` - Create snapshot and initial migration
127
+ - `-c, --config <path>` - Path to zenstack-kit config file
127
128
 
128
129
  ### `zenstack-kit migrate:generate`
129
130
 
@@ -138,6 +139,7 @@ Options:
138
139
  - `-s, --schema <path>` - Path to ZenStack schema
139
140
  - `-m, --migrations <path>` - Migrations directory
140
141
  - `--dialect <dialect>` - Database dialect (`sqlite`, `postgres`, `mysql`)
142
+ - `-c, --config <path>` - Path to zenstack-kit config file
141
143
 
142
144
  ### `zenstack-kit migrate:apply`
143
145
 
@@ -153,6 +155,8 @@ Options:
153
155
  - `--url <url>` - Database connection URL (overrides config)
154
156
  - `--table <name>` - Migrations table name (default: `_prisma_migrations`)
155
157
  - `--db-schema <name>` - Database schema for migrations table (PostgreSQL only, default: `public`)
158
+ - `--preview` - Preview pending migrations without applying
159
+ - `-c, --config <path>` - Path to zenstack-kit config file
156
160
 
157
161
  ### `zenstack-kit pull`
158
162
 
@@ -166,6 +170,7 @@ Options:
166
170
  - `-o, --output <path>` - Output path for schema (default: `./schema.zmodel`)
167
171
  - `--dialect <dialect>` - Database dialect
168
172
  - `--url <url>` - Database connection URL
173
+ - `-c, --config <path>` - Path to zenstack-kit config file
169
174
 
170
175
  Features:
171
176
  - Detects tables, columns, and types
@@ -289,6 +294,11 @@ const { db, destroy } = await createKyselyAdapter({
289
294
  await destroy();
290
295
  ```
291
296
 
297
+ ## Experimental
298
+
299
+ The `introspectSchema` API is experimental and uses a simplified parser. Expect
300
+ limitations with complex schemas.
301
+
292
302
  ## Prisma Compatibility
293
303
 
294
304
  zenstack-kit is designed to be compatible with Prisma's migration system:
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/cli/app.tsx"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;AAuSH,wBAAgB,MAAM,SAkBrB"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/cli/app.tsx"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;AA0SH,wBAAgB,MAAM,SAkBrB"}
package/dist/cli/app.js CHANGED
@@ -77,6 +77,9 @@ function parseArgs() {
77
77
  else if (arg === "--force" || arg === "-f") {
78
78
  options.force = true;
79
79
  }
80
+ else if (arg === "--config" || arg === "-c") {
81
+ options.config = args[++i];
82
+ }
80
83
  }
81
84
  return { command, options };
82
85
  }
@@ -98,7 +101,7 @@ function Status({ type, message }) {
98
101
  }
99
102
  // Help display component
100
103
  function HelpDisplay() {
101
- return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "zenstack-kit" }), _jsx(Text, { dimColor: true, children: "Database tooling for ZenStack schemas" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Commands:" }), commands.filter(c => c.value !== "exit").map((cmd) => (_jsxs(Box, { marginLeft: 2, children: [_jsx(Box, { width: 20, children: _jsx(Text, { color: "yellow", children: cmd.label }) }), _jsx(Text, { dimColor: true, children: cmd.description })] }, cmd.value))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Options:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "-s, --schema <path> Path to ZenStack schema" }), _jsx(Text, { dimColor: true, children: "-m, --migrations <path> Migrations directory" }), _jsx(Text, { dimColor: true, children: "-n, --name <name> Migration name" }), _jsx(Text, { dimColor: true, children: "--dialect <dialect> Database dialect (sqlite, postgres, mysql)" }), _jsx(Text, { dimColor: true, children: "--url <url> Database connection URL" }), _jsx(Text, { dimColor: true, children: "--create-initial Create initial migration (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--baseline Create baseline only (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--preview Preview pending migrations without applying" }), _jsx(Text, { dimColor: true, children: "-f, --force Force operation without confirmation" })] })] }));
104
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "zenstack-kit" }), _jsx(Text, { dimColor: true, children: "Database tooling for ZenStack schemas" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Commands:" }), commands.filter(c => c.value !== "exit").map((cmd) => (_jsxs(Box, { marginLeft: 2, children: [_jsx(Box, { width: 20, children: _jsx(Text, { color: "yellow", children: cmd.label }) }), _jsx(Text, { dimColor: true, children: cmd.description })] }, cmd.value))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Options:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "-s, --schema <path> Path to ZenStack schema" }), _jsx(Text, { dimColor: true, children: "-m, --migrations <path> Migrations directory" }), _jsx(Text, { dimColor: true, children: "-n, --name <name> Migration name" }), _jsx(Text, { dimColor: true, children: "--dialect <dialect> Database dialect (sqlite, postgres, mysql)" }), _jsx(Text, { dimColor: true, children: "--url <url> Database connection URL" }), _jsx(Text, { dimColor: true, children: "--create-initial Create initial migration (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--baseline Create baseline only (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--preview Preview pending migrations without applying" }), _jsx(Text, { dimColor: true, children: "-f, --force Force operation without confirmation" }), _jsx(Text, { dimColor: true, children: "-c, --config <path> Path to zenstack-kit config file" })] })] }));
102
105
  }
103
106
  function CliApp({ initialCommand, options }) {
104
107
  const { exit } = useApp();
@@ -20,6 +20,7 @@ export interface CommandOptions {
20
20
  createInitial?: boolean;
21
21
  preview?: boolean;
22
22
  force?: boolean;
23
+ config?: string;
23
24
  }
24
25
  export interface CommandContext {
25
26
  cwd: string;
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
@@ -19,8 +19,11 @@ export class CommandError extends Error {
19
19
  * Load and validate config, returning resolved paths
20
20
  */
21
21
  export async function resolveConfig(ctx) {
22
- const loaded = await loadConfig(ctx.cwd);
22
+ const loaded = await loadConfig(ctx.cwd, ctx.options.config);
23
23
  if (!loaded) {
24
+ if (ctx.options.config) {
25
+ throw new CommandError(`Config file not found: ${ctx.options.config}`);
26
+ }
24
27
  throw new CommandError("No zenstack-kit config file found.");
25
28
  }
26
29
  const { config, configDir } = loaded;
@@ -121,6 +124,7 @@ export async function runMigrateGenerate(ctx) {
121
124
  }
122
125
  ctx.log("success", `Migration created: ${migration.folderName}/migration.sql`);
123
126
  ctx.log("info", `Path: ${migration.folderPath}`);
127
+ ctx.log("info", "Next: run 'zenstack-kit migrate apply' (or --preview to review SQL).");
124
128
  }
125
129
  /**
126
130
  * migrate:apply command
@@ -146,7 +150,7 @@ export async function runMigrateApply(ctx) {
146
150
  const databasePath = dialect === "sqlite" ? connectionUrl : undefined;
147
151
  // Preview mode - show pending migrations without applying
148
152
  if (ctx.options.preview) {
149
- ctx.log("info", "Preview mode - showing pending migrations:");
153
+ ctx.log("info", "Preview mode - no changes will be applied.");
150
154
  const preview = await previewPrismaMigrations({
151
155
  migrationsFolder: outputPath,
152
156
  dialect,
@@ -162,8 +166,12 @@ export async function runMigrateApply(ctx) {
162
166
  }
163
167
  return;
164
168
  }
169
+ ctx.log("info", `Pending migrations: ${preview.pending.length}`);
170
+ if (preview.alreadyApplied.length > 0) {
171
+ ctx.log("info", `${preview.alreadyApplied.length} migration(s) already applied`);
172
+ }
165
173
  for (const migration of preview.pending) {
166
- ctx.log("info", `Pending: ${migration.name}`);
174
+ ctx.log("info", `Migration: ${migration.name}`);
167
175
  ctx.log("info", `SQL:\n${migration.sql}`);
168
176
  }
169
177
  return;
@@ -335,6 +343,7 @@ export async function runPull(ctx) {
335
343
  });
336
344
  ctx.log("success", `Schema generated: ${result.outputPath}`);
337
345
  ctx.log("info", `${result.tableCount} table(s) introspected`);
346
+ ctx.log("info", "Next: review the schema, then run 'zenstack-kit init' to reset the snapshot.");
338
347
  // If we have existing migrations, warn about resetting
339
348
  if (snapshotExists || migrations.length > 0) {
340
349
  ctx.log("warning", "You should run 'zenstack-kit init' to reset the snapshot after reviewing the schema.");
@@ -7,5 +7,5 @@ export interface LoadedConfig {
7
7
  configPath: string;
8
8
  configDir: string;
9
9
  }
10
- export declare function loadConfig(cwd: string): Promise<LoadedConfig | null>;
10
+ export declare function loadConfig(cwd: string, configPath?: string): Promise<LoadedConfig | null>;
11
11
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAUpD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA4B1E"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAUpD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA+B/F"}
@@ -10,32 +10,34 @@ const CONFIG_FILES = [
10
10
  "zenstack-kit.config.mjs",
11
11
  "zenstack-kit.config.cjs",
12
12
  ];
13
- export async function loadConfig(cwd) {
14
- const configPath = CONFIG_FILES.map((file) => path.join(cwd, file)).find((file) => fs.existsSync(file));
15
- if (!configPath) {
13
+ export async function loadConfig(cwd, configPath) {
14
+ const resolvedConfigPath = configPath ? path.resolve(cwd, configPath) : null;
15
+ const configPathToLoad = resolvedConfigPath ??
16
+ CONFIG_FILES.map((file) => path.join(cwd, file)).find((file) => fs.existsSync(file));
17
+ if (!configPathToLoad) {
16
18
  return null;
17
19
  }
18
- const ext = path.extname(configPath);
20
+ const ext = path.extname(configPathToLoad);
19
21
  let config;
20
22
  if (ext === ".cjs") {
21
23
  const require = createRequire(import.meta.url);
22
- const loaded = require(configPath);
24
+ const loaded = require(configPathToLoad);
23
25
  config = (loaded.default ?? loaded);
24
26
  }
25
27
  else if (ext === ".js" || ext === ".mjs") {
26
- const loaded = await import(pathToFileUrl(configPath));
28
+ const loaded = await import(pathToFileUrl(configPathToLoad));
27
29
  config = (loaded.default ?? loaded);
28
30
  }
29
31
  else {
30
32
  const { default: jiti } = await import("jiti");
31
33
  const loader = jiti(import.meta.url, { interopDefault: true });
32
- const loaded = loader(configPath);
34
+ const loaded = loader(configPathToLoad);
33
35
  config = (loaded.default ?? loaded);
34
36
  }
35
37
  return {
36
38
  config,
37
- configPath,
38
- configDir: path.dirname(configPath),
39
+ configPath: configPathToLoad,
40
+ configDir: path.dirname(configPathToLoad),
39
41
  };
40
42
  }
41
43
  function pathToFileUrl(filePath) {
@@ -0,0 +1,52 @@
1
+ import type { KyselyDialect } from "../../sql/kysely-adapter.js";
2
+ export interface ApplyPrismaMigrationsOptions {
3
+ /** Migrations folder path */
4
+ migrationsFolder: string;
5
+ /** Database dialect */
6
+ dialect: KyselyDialect;
7
+ /** Database connection URL */
8
+ connectionUrl?: string;
9
+ /** SQLite database path */
10
+ databasePath?: string;
11
+ /** Migrations table name (default: _prisma_migrations) */
12
+ migrationsTable?: string;
13
+ /** Migrations schema (PostgreSQL only, default: public) */
14
+ migrationsSchema?: string;
15
+ }
16
+ export interface ApplyPrismaMigrationsResult {
17
+ applied: Array<{
18
+ migrationName: string;
19
+ duration: number;
20
+ }>;
21
+ alreadyApplied: string[];
22
+ failed?: {
23
+ migrationName: string;
24
+ error: string;
25
+ };
26
+ coherenceErrors?: MigrationCoherenceError[];
27
+ }
28
+ export interface MigrationCoherenceError {
29
+ type: "missing_from_log" | "missing_from_db" | "missing_from_disk" | "order_mismatch" | "checksum_mismatch";
30
+ migrationName: string;
31
+ details: string;
32
+ }
33
+ export interface MigrationCoherenceResult {
34
+ isCoherent: boolean;
35
+ errors: MigrationCoherenceError[];
36
+ }
37
+ export interface PreviewPrismaMigrationsResult {
38
+ pending: Array<{
39
+ name: string;
40
+ sql: string;
41
+ }>;
42
+ alreadyApplied: string[];
43
+ }
44
+ /**
45
+ * Apply pending Prisma migrations
46
+ */
47
+ export declare function applyPrismaMigrations(options: ApplyPrismaMigrationsOptions): Promise<ApplyPrismaMigrationsResult>;
48
+ /**
49
+ * Preview pending migrations without applying them
50
+ */
51
+ export declare function previewPrismaMigrations(options: ApplyPrismaMigrationsOptions): Promise<PreviewPrismaMigrationsResult>;
52
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/migrations/prisma/apply.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAIjE,MAAM,WAAW,4BAA4B;IAC3C,6BAA6B;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,uBAAuB;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,KAAK,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,eAAe,CAAC,EAAE,uBAAuB,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,kBAAkB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;IAC5G,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,uBAAuB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AA2QD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAuHtC;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,6BAA6B,CAAC,CA0DxC"}
@@ -0,0 +1,382 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import * as crypto from "crypto";
4
+ import { sql } from "kysely";
5
+ import { createKyselyAdapter } from "../../sql/kysely-adapter.js";
6
+ import { calculateChecksum, readMigrationLog } from "./log.js";
7
+ /**
8
+ * Ensure _prisma_migrations table exists
9
+ */
10
+ async function ensureMigrationsTable(db, tableName, schema, dialect) {
11
+ const fullTableName = schema && dialect === "postgres" ? `${schema}.${tableName}` : tableName;
12
+ if (dialect === "sqlite") {
13
+ await sql `
14
+ CREATE TABLE IF NOT EXISTS ${sql.raw(`"${tableName}"`)} (
15
+ id TEXT PRIMARY KEY,
16
+ checksum TEXT NOT NULL,
17
+ finished_at TEXT,
18
+ migration_name TEXT NOT NULL,
19
+ logs TEXT,
20
+ rolled_back_at TEXT,
21
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
22
+ applied_steps_count INTEGER NOT NULL DEFAULT 0
23
+ )
24
+ `.execute(db);
25
+ }
26
+ else if (dialect === "postgres") {
27
+ await sql `
28
+ CREATE TABLE IF NOT EXISTS ${sql.raw(`"${schema}"."${tableName}"`)} (
29
+ id VARCHAR(36) PRIMARY KEY,
30
+ checksum VARCHAR(64) NOT NULL,
31
+ finished_at TIMESTAMPTZ,
32
+ migration_name VARCHAR(255) NOT NULL,
33
+ logs TEXT,
34
+ rolled_back_at TIMESTAMPTZ,
35
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
36
+ applied_steps_count INTEGER NOT NULL DEFAULT 0
37
+ )
38
+ `.execute(db);
39
+ }
40
+ else {
41
+ await sql `
42
+ CREATE TABLE IF NOT EXISTS ${sql.raw(`\`${tableName}\``)} (
43
+ id VARCHAR(36) PRIMARY KEY,
44
+ checksum VARCHAR(64) NOT NULL,
45
+ finished_at DATETIME,
46
+ migration_name VARCHAR(255) NOT NULL,
47
+ logs TEXT,
48
+ rolled_back_at DATETIME,
49
+ started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
50
+ applied_steps_count INTEGER NOT NULL DEFAULT 0
51
+ )
52
+ `.execute(db);
53
+ }
54
+ }
55
+ /**
56
+ * Get list of applied migrations from _prisma_migrations table
57
+ */
58
+ async function getAppliedMigrations(db, tableName, schema, dialect) {
59
+ let result;
60
+ if (dialect === "postgres" && schema) {
61
+ result = await sql `
62
+ SELECT * FROM ${sql.raw(`"${schema}"."${tableName}"`)}
63
+ WHERE rolled_back_at IS NULL AND finished_at IS NOT NULL
64
+ `.execute(db);
65
+ }
66
+ else if (dialect === "sqlite") {
67
+ result = await sql `
68
+ SELECT * FROM ${sql.raw(`"${tableName}"`)}
69
+ WHERE rolled_back_at IS NULL AND finished_at IS NOT NULL
70
+ `.execute(db);
71
+ }
72
+ else {
73
+ result = await sql `
74
+ SELECT * FROM ${sql.raw(`\`${tableName}\``)}
75
+ WHERE rolled_back_at IS NULL AND finished_at IS NOT NULL
76
+ `.execute(db);
77
+ }
78
+ return new Map(result.rows.map((row) => [row.migration_name, row]));
79
+ }
80
+ /**
81
+ * Record a migration in _prisma_migrations table
82
+ */
83
+ async function recordMigration(db, tableName, schema, dialect, migrationName, checksum) {
84
+ const id = crypto.randomUUID();
85
+ if (dialect === "postgres" && schema) {
86
+ await sql `
87
+ INSERT INTO ${sql.raw(`"${schema}"."${tableName}"`)} (id, checksum, migration_name, finished_at, applied_steps_count)
88
+ VALUES (${id}, ${checksum}, ${migrationName}, now(), 1)
89
+ `.execute(db);
90
+ }
91
+ else if (dialect === "sqlite") {
92
+ await sql `
93
+ INSERT INTO ${sql.raw(`"${tableName}"`)} (id, checksum, migration_name, finished_at, applied_steps_count)
94
+ VALUES (${id}, ${checksum}, ${migrationName}, datetime('now'), 1)
95
+ `.execute(db);
96
+ }
97
+ else {
98
+ await sql `
99
+ INSERT INTO ${sql.raw(`\`${tableName}\``)} (id, checksum, migration_name, finished_at, applied_steps_count)
100
+ VALUES (${id}, ${checksum}, ${migrationName}, NOW(), 1)
101
+ `.execute(db);
102
+ }
103
+ }
104
+ /**
105
+ * Validate that the database's applied migrations are coherent with the migration log.
106
+ *
107
+ * Coherence rules:
108
+ * 1. Every migration applied in the DB must exist in the migration log
109
+ * 2. Applied migrations must be a prefix of the log (no gaps)
110
+ * 3. Checksums must match for applied migrations
111
+ */
112
+ function validateMigrationCoherence(appliedMigrations, migrationLog, migrationFolders) {
113
+ const errors = [];
114
+ // Build a set of log migration names for quick lookup
115
+ const logMigrationNames = new Set(migrationLog.map((e) => e.name));
116
+ const folderNames = new Set(migrationFolders);
117
+ for (const entry of migrationLog) {
118
+ if (!folderNames.has(entry.name)) {
119
+ errors.push({
120
+ type: "missing_from_disk",
121
+ migrationName: entry.name,
122
+ details: `Migration "${entry.name}" exists in migration log but not on disk`,
123
+ });
124
+ }
125
+ }
126
+ // Check 1: Every applied migration must exist in the log
127
+ for (const [migrationName] of appliedMigrations) {
128
+ if (!logMigrationNames.has(migrationName)) {
129
+ errors.push({
130
+ type: "missing_from_log",
131
+ migrationName,
132
+ details: `Migration "${migrationName}" exists in database but not in migration log`,
133
+ });
134
+ }
135
+ }
136
+ // If there are migrations missing from the log, return early
137
+ // (other checks don't make sense if the log is incomplete)
138
+ if (errors.length > 0) {
139
+ return { isCoherent: false, errors };
140
+ }
141
+ // Check 2: Applied migrations should be a continuous prefix of the log
142
+ // i.e., if migration N is applied, all migrations before N in the log must also be applied
143
+ let lastAppliedIndex = -1;
144
+ for (let i = 0; i < migrationLog.length; i++) {
145
+ const logEntry = migrationLog[i];
146
+ const isApplied = appliedMigrations.has(logEntry.name);
147
+ if (isApplied) {
148
+ // Check for gaps: if this is applied, all previous should be applied
149
+ if (lastAppliedIndex !== i - 1) {
150
+ // There's a gap - find the missing migrations
151
+ for (let j = lastAppliedIndex + 1; j < i; j++) {
152
+ const missing = migrationLog[j];
153
+ errors.push({
154
+ type: "order_mismatch",
155
+ migrationName: missing.name,
156
+ details: `Migration "${missing.name}" is in the log but not applied, yet later migration "${logEntry.name}" is applied`,
157
+ });
158
+ }
159
+ }
160
+ lastAppliedIndex = i;
161
+ // Check 3: Checksum validation for applied migrations
162
+ const dbRow = appliedMigrations.get(logEntry.name);
163
+ if (dbRow.checksum !== logEntry.checksum) {
164
+ errors.push({
165
+ type: "checksum_mismatch",
166
+ migrationName: logEntry.name,
167
+ details: `Checksum mismatch for "${logEntry.name}": database has ${dbRow.checksum.slice(0, 8)}..., log has ${logEntry.checksum.slice(0, 8)}...`,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ return {
173
+ isCoherent: errors.length === 0,
174
+ errors,
175
+ };
176
+ }
177
+ /**
178
+ * Execute raw SQL using the database driver directly
179
+ * This bypasses Kysely for DDL statements which don't work reliably with sql.raw()
180
+ */
181
+ async function executeRawSql(dialect, sqlContent, options) {
182
+ if (dialect === "sqlite") {
183
+ const { default: Database } = await import("better-sqlite3");
184
+ const sqliteDb = new Database(options.databasePath || ":memory:");
185
+ try {
186
+ // better-sqlite3's exec() handles multiple statements properly
187
+ sqliteDb.exec(sqlContent);
188
+ }
189
+ finally {
190
+ sqliteDb.close();
191
+ }
192
+ }
193
+ else if (dialect === "postgres") {
194
+ const { Pool } = await import("pg");
195
+ const pool = new Pool({ connectionString: options.connectionUrl });
196
+ const client = await pool.connect();
197
+ try {
198
+ // PostgreSQL supports transactional DDL, so wrap migration in a transaction
199
+ await client.query("BEGIN");
200
+ await client.query(sqlContent);
201
+ await client.query("COMMIT");
202
+ }
203
+ catch (error) {
204
+ await client.query("ROLLBACK");
205
+ throw error;
206
+ }
207
+ finally {
208
+ client.release();
209
+ await pool.end();
210
+ }
211
+ }
212
+ else if (dialect === "mysql") {
213
+ // Use mysql2 with promise wrapper
214
+ const mysql = await import("mysql2");
215
+ const pool = mysql.createPool({ uri: options.connectionUrl });
216
+ const promisePool = pool.promise();
217
+ try {
218
+ // MySQL needs statements executed one at a time
219
+ const statements = sqlContent
220
+ .split(/;(?:\s*\n|\s*$)/)
221
+ .map((s) => s.trim())
222
+ .filter((s) => s.length > 0 && !s.startsWith("--"));
223
+ for (const statement of statements) {
224
+ await promisePool.query(statement);
225
+ }
226
+ }
227
+ finally {
228
+ await pool.promise().end();
229
+ }
230
+ }
231
+ }
232
+ /**
233
+ * Apply pending Prisma migrations
234
+ */
235
+ export async function applyPrismaMigrations(options) {
236
+ const migrationsTable = options.migrationsTable ?? "_prisma_migrations";
237
+ const migrationsSchema = options.migrationsSchema ?? "public";
238
+ const { db, destroy } = await createKyselyAdapter({
239
+ dialect: options.dialect,
240
+ connectionUrl: options.connectionUrl,
241
+ databasePath: options.databasePath,
242
+ });
243
+ try {
244
+ // Ensure migrations table exists
245
+ await ensureMigrationsTable(db, migrationsTable, migrationsSchema, options.dialect);
246
+ // Get already applied migrations
247
+ const appliedMigrations = await getAppliedMigrations(db, migrationsTable, migrationsSchema, options.dialect);
248
+ // Read migration folders
249
+ const entries = await fs.readdir(options.migrationsFolder, { withFileTypes: true });
250
+ const migrationFolders = entries
251
+ .filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
252
+ .map((e) => e.name)
253
+ .sort();
254
+ const migrationFoldersWithSql = [];
255
+ for (const folderName of migrationFolders) {
256
+ const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
257
+ try {
258
+ await fs.access(sqlPath);
259
+ migrationFoldersWithSql.push(folderName);
260
+ }
261
+ catch {
262
+ // Missing migration.sql; coherence check will flag if it's in the log
263
+ }
264
+ }
265
+ // Read migration log and validate coherence
266
+ const migrationLog = await readMigrationLog(options.migrationsFolder);
267
+ const coherence = validateMigrationCoherence(appliedMigrations, migrationLog, migrationFoldersWithSql);
268
+ if (!coherence.isCoherent) {
269
+ return {
270
+ applied: [],
271
+ alreadyApplied: [],
272
+ coherenceErrors: coherence.errors,
273
+ };
274
+ }
275
+ const result = {
276
+ applied: [],
277
+ alreadyApplied: [],
278
+ };
279
+ for (const folderName of migrationFoldersWithSql) {
280
+ if (appliedMigrations.has(folderName)) {
281
+ result.alreadyApplied.push(folderName);
282
+ continue;
283
+ }
284
+ const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
285
+ let sqlContent;
286
+ try {
287
+ sqlContent = await fs.readFile(sqlPath, "utf-8");
288
+ }
289
+ catch {
290
+ continue; // Skip if no migration.sql
291
+ }
292
+ const checksum = calculateChecksum(sqlContent);
293
+ // Verify checksum against migration log (migrationLog already read above)
294
+ const logEntry = migrationLog.find((m) => m.name === folderName);
295
+ if (logEntry && logEntry.checksum !== checksum) {
296
+ result.failed = {
297
+ migrationName: folderName,
298
+ error: `Checksum mismatch for migration ${folderName}.\n` +
299
+ `Expected: ${logEntry.checksum}\n` +
300
+ `Found: ${checksum}\n` +
301
+ `The migration file may have been modified after generation.`,
302
+ };
303
+ break;
304
+ }
305
+ const startTime = Date.now();
306
+ try {
307
+ // Execute the migration SQL using direct driver access
308
+ await executeRawSql(options.dialect, sqlContent, {
309
+ connectionUrl: options.connectionUrl,
310
+ databasePath: options.databasePath,
311
+ });
312
+ // Record the migration (still use Kysely for this since it's simple INSERT)
313
+ await recordMigration(db, migrationsTable, migrationsSchema, options.dialect, folderName, checksum);
314
+ result.applied.push({
315
+ migrationName: folderName,
316
+ duration: Date.now() - startTime,
317
+ });
318
+ }
319
+ catch (error) {
320
+ result.failed = {
321
+ migrationName: folderName,
322
+ error: error instanceof Error ? error.message : String(error),
323
+ };
324
+ break; // Stop on first failure
325
+ }
326
+ }
327
+ return result;
328
+ }
329
+ finally {
330
+ await destroy();
331
+ }
332
+ }
333
+ /**
334
+ * Preview pending migrations without applying them
335
+ */
336
+ export async function previewPrismaMigrations(options) {
337
+ const migrationsTable = options.migrationsTable ?? "_prisma_migrations";
338
+ const migrationsSchema = options.migrationsSchema ?? "public";
339
+ const { db, destroy } = await createKyselyAdapter({
340
+ dialect: options.dialect,
341
+ connectionUrl: options.connectionUrl,
342
+ databasePath: options.databasePath,
343
+ });
344
+ try {
345
+ // Ensure migrations table exists
346
+ await ensureMigrationsTable(db, migrationsTable, migrationsSchema, options.dialect);
347
+ // Get already applied migrations
348
+ const appliedMigrations = await getAppliedMigrations(db, migrationsTable, migrationsSchema, options.dialect);
349
+ // Read migration folders
350
+ const entries = await fs.readdir(options.migrationsFolder, { withFileTypes: true });
351
+ const migrationFolders = entries
352
+ .filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
353
+ .map((e) => e.name)
354
+ .sort();
355
+ const result = {
356
+ pending: [],
357
+ alreadyApplied: [],
358
+ };
359
+ for (const folderName of migrationFolders) {
360
+ if (appliedMigrations.has(folderName)) {
361
+ result.alreadyApplied.push(folderName);
362
+ continue;
363
+ }
364
+ const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
365
+ let sqlContent;
366
+ try {
367
+ sqlContent = await fs.readFile(sqlPath, "utf-8");
368
+ }
369
+ catch {
370
+ continue; // Skip if no migration.sql
371
+ }
372
+ result.pending.push({
373
+ name: folderName,
374
+ sql: sqlContent,
375
+ });
376
+ }
377
+ return result;
378
+ }
379
+ finally {
380
+ await destroy();
381
+ }
382
+ }