@zmdb/migrations 1.0.0-beta.1

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 (114) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +36 -0
  3. package/dist/declarations/emit.d.ts +23 -0
  4. package/dist/declarations/emit.d.ts.map +1 -0
  5. package/dist/declarations/emit.js +581 -0
  6. package/dist/declarations/emit.js.map +1 -0
  7. package/dist/declarations/index.d.ts +3 -0
  8. package/dist/declarations/index.d.ts.map +1 -0
  9. package/dist/declarations/index.js +3 -0
  10. package/dist/declarations/index.js.map +1 -0
  11. package/dist/declarations/tagged-property.d.ts +33 -0
  12. package/dist/declarations/tagged-property.d.ts.map +1 -0
  13. package/dist/declarations/tagged-property.js +148 -0
  14. package/dist/declarations/tagged-property.js.map +1 -0
  15. package/dist/embedded.d.ts +25 -0
  16. package/dist/embedded.d.ts.map +1 -0
  17. package/dist/embedded.js +133 -0
  18. package/dist/embedded.js.map +1 -0
  19. package/dist/file-io.d.ts +15 -0
  20. package/dist/file-io.d.ts.map +1 -0
  21. package/dist/file-io.js +92 -0
  22. package/dist/file-io.js.map +1 -0
  23. package/dist/files.d.ts +11 -0
  24. package/dist/files.d.ts.map +1 -0
  25. package/dist/files.js +10 -0
  26. package/dist/files.js.map +1 -0
  27. package/dist/index.d.ts +70 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +350 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/introspect/common.d.ts +26 -0
  32. package/dist/introspect/common.d.ts.map +1 -0
  33. package/dist/introspect/common.js +155 -0
  34. package/dist/introspect/common.js.map +1 -0
  35. package/dist/introspect/drift.d.ts +45 -0
  36. package/dist/introspect/drift.d.ts.map +1 -0
  37. package/dist/introspect/drift.js +84 -0
  38. package/dist/introspect/drift.js.map +1 -0
  39. package/dist/introspect/index.d.ts +8 -0
  40. package/dist/introspect/index.d.ts.map +1 -0
  41. package/dist/introspect/index.js +10 -0
  42. package/dist/introspect/index.js.map +1 -0
  43. package/dist/operations/check.d.ts +16 -0
  44. package/dist/operations/check.d.ts.map +1 -0
  45. package/dist/operations/check.js +236 -0
  46. package/dist/operations/check.js.map +1 -0
  47. package/dist/operations/embed.d.ts +23 -0
  48. package/dist/operations/embed.d.ts.map +1 -0
  49. package/dist/operations/embed.js +52 -0
  50. package/dist/operations/embed.js.map +1 -0
  51. package/dist/operations/export.d.ts +9 -0
  52. package/dist/operations/export.d.ts.map +1 -0
  53. package/dist/operations/export.js +15 -0
  54. package/dist/operations/export.js.map +1 -0
  55. package/dist/operations/generate.d.ts +17 -0
  56. package/dist/operations/generate.d.ts.map +1 -0
  57. package/dist/operations/generate.js +115 -0
  58. package/dist/operations/generate.js.map +1 -0
  59. package/dist/operations/migrate.d.ts +23 -0
  60. package/dist/operations/migrate.d.ts.map +1 -0
  61. package/dist/operations/migrate.js +67 -0
  62. package/dist/operations/migrate.js.map +1 -0
  63. package/dist/operations/pull.d.ts +40 -0
  64. package/dist/operations/pull.d.ts.map +1 -0
  65. package/dist/operations/pull.js +100 -0
  66. package/dist/operations/pull.js.map +1 -0
  67. package/dist/operations/push.d.ts +20 -0
  68. package/dist/operations/push.d.ts.map +1 -0
  69. package/dist/operations/push.js +82 -0
  70. package/dist/operations/push.js.map +1 -0
  71. package/dist/operations/upgrade.d.ts +10 -0
  72. package/dist/operations/upgrade.d.ts.map +1 -0
  73. package/dist/operations/upgrade.js +38 -0
  74. package/dist/operations/upgrade.js.map +1 -0
  75. package/dist/project.d.ts +46 -0
  76. package/dist/project.d.ts.map +1 -0
  77. package/dist/project.js +42 -0
  78. package/dist/project.js.map +1 -0
  79. package/dist/runner.d.ts +59 -0
  80. package/dist/runner.d.ts.map +1 -0
  81. package/dist/runner.js +163 -0
  82. package/dist/runner.js.map +1 -0
  83. package/dist/testing/official-dialects.fixture.d.ts +16 -0
  84. package/dist/testing/official-dialects.fixture.d.ts.map +1 -0
  85. package/dist/testing/official-dialects.fixture.js +21 -0
  86. package/dist/testing/official-dialects.fixture.js.map +1 -0
  87. package/dist/testing.d.ts +8 -0
  88. package/dist/testing.d.ts.map +1 -0
  89. package/dist/testing.js +27 -0
  90. package/dist/testing.js.map +1 -0
  91. package/package.json +81 -0
  92. package/src/declarations/emit.ts +705 -0
  93. package/src/declarations/index.ts +13 -0
  94. package/src/declarations/tagged-property.ts +163 -0
  95. package/src/embedded.ts +192 -0
  96. package/src/file-io.ts +114 -0
  97. package/src/files.ts +34 -0
  98. package/src/index.ts +502 -0
  99. package/src/introspect/__fixtures__/mysql-8.4.11.json +371 -0
  100. package/src/introspect/common.ts +203 -0
  101. package/src/introspect/drift.ts +157 -0
  102. package/src/introspect/index.ts +33 -0
  103. package/src/operations/check.ts +309 -0
  104. package/src/operations/embed.ts +87 -0
  105. package/src/operations/export.ts +21 -0
  106. package/src/operations/generate.ts +144 -0
  107. package/src/operations/migrate.ts +110 -0
  108. package/src/operations/pull.ts +160 -0
  109. package/src/operations/push.ts +98 -0
  110. package/src/operations/upgrade.ts +56 -0
  111. package/src/project.ts +93 -0
  112. package/src/runner.ts +260 -0
  113. package/src/testing/official-dialects.fixture.ts +24 -0
  114. package/src/testing.ts +32 -0
package/src/runner.ts ADDED
@@ -0,0 +1,260 @@
1
+ // #44 — async migration runner + version tracking + driver adapter.
2
+ // Applies/rolls back ordered migrations against an async connection or
3
+ // adapted database driver, recording applied versions in _zmdb_migrations.
4
+
5
+ import {
6
+ dialectName,
7
+ type CompiledQuery,
8
+ type MigrationDriver as DialectMigrationDriver,
9
+ type SqlDialect,
10
+ } from '@zmdb/sql';
11
+
12
+ export interface Migration {
13
+ readonly version: number;
14
+ readonly name: string;
15
+ readonly up: string;
16
+ readonly down: string;
17
+ }
18
+
19
+ export interface AppliedMigration {
20
+ readonly version: number;
21
+ readonly name: string;
22
+ /** `null` identifies a ledger row written before checksums were introduced. */
23
+ readonly checksum: string | null;
24
+ }
25
+
26
+ // Minimal async-compatible connection the runner needs.
27
+ export interface MigrationConnection {
28
+ readonly dialect?: string | SqlDialect;
29
+ /** False when DDL commits independently of BEGIN/ROLLBACK, as it does on MySQL. */
30
+ readonly transactionalDdl?: boolean;
31
+ exec(sql: string): Promise<void> | void;
32
+ appliedVersions(): Promise<readonly number[]> | readonly number[];
33
+ appliedMigrations?(): Promise<readonly AppliedMigration[]> | readonly AppliedMigration[];
34
+ recordApplied(version: number, name: string, checksum?: string): Promise<void> | void;
35
+ recordReverted(version: number): Promise<void> | void;
36
+ ensureVersionTable?(): Promise<void> | void;
37
+ checksum?(sql: string): Promise<string> | string;
38
+ transaction?<T>(run: (connection?: MigrationConnection) => Promise<T>): Promise<T>;
39
+ }
40
+
41
+ // Interface matching any runtime database driver (e.g. from @zmdb/orm).
42
+ export interface MigrationDriver {
43
+ readonly dialect: SqlDialect;
44
+ execute(query: CompiledQuery): Promise<readonly Record<string, unknown>[]>;
45
+ transaction?<T>(run: (driver: MigrationDriver) => Promise<T>): Promise<T>;
46
+ }
47
+
48
+ export interface MigrationTableOptions {
49
+ readonly table?: string;
50
+ readonly schema?: string;
51
+ }
52
+
53
+ export interface MigrationRunOptions {
54
+ readonly onWarning?: (message: string) => void;
55
+ }
56
+
57
+ export interface MigrationStatus {
58
+ readonly version: number;
59
+ readonly name: string;
60
+ readonly applied: boolean;
61
+ }
62
+
63
+ const DEFAULT_VERSION_TABLE = `CREATE TABLE IF NOT EXISTS _zmdb_migrations (
64
+ version INTEGER PRIMARY KEY,
65
+ name TEXT NOT NULL,
66
+ applied_at INTEGER NOT NULL,
67
+ checksum TEXT
68
+ )`;
69
+
70
+ function nonTransactionalDdlWarning(dialect: string | SqlDialect | undefined): string {
71
+ const database =
72
+ dialect === undefined ? 'the configured database' : typeof dialect === 'string' ? dialect : dialectName(dialect);
73
+ return `${database} does not support transactional DDL; a failed migration may leave its schema changes partially applied even though its ledger row is absent`;
74
+ }
75
+
76
+ function dialectMigrationDriver<Name extends string>(
77
+ dialect: SqlDialect<Name>,
78
+ driver: MigrationDriver,
79
+ ): DialectMigrationDriver<Name> {
80
+ const transaction = driver.transaction;
81
+ return {
82
+ dialect,
83
+ execute: query => driver.execute(query),
84
+ ...(transaction === undefined
85
+ ? {}
86
+ : {
87
+ transaction: <Result>(
88
+ run: (transactionDriver: DialectMigrationDriver<Name>) => Promise<Result>,
89
+ ): Promise<Result> => transaction(nested => run(dialectMigrationDriver(dialect, nested))),
90
+ }),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Adapt any runtime database driver instance (e.g. PostgreSQL, SQLite)
96
+ * into an asynchronous MigrationConnection.
97
+ */
98
+ export function driverMigrationConnection(
99
+ driver: MigrationDriver,
100
+ dialect: SqlDialect = driver.dialect,
101
+ options: MigrationTableOptions = {},
102
+ ): MigrationConnection {
103
+ return dialect.migrations.connection(dialectMigrationDriver(dialect, driver), options);
104
+ }
105
+
106
+ export async function ensureVersionTable(conn: MigrationConnection): Promise<void> {
107
+ if (conn.ensureVersionTable !== undefined) {
108
+ await conn.ensureVersionTable();
109
+ return;
110
+ }
111
+ await conn.exec(DEFAULT_VERSION_TABLE);
112
+ }
113
+
114
+ /** Apply all pending migrations in ascending version order. */
115
+ export async function up(
116
+ conn: MigrationConnection,
117
+ migrations: readonly Migration[],
118
+ options: MigrationRunOptions = {},
119
+ ): Promise<number[]> {
120
+ assertUniqueVersions(migrations);
121
+ await ensureVersionTable(conn);
122
+ const ledger = await verifiedLedger(conn, migrations);
123
+ const applied = new Set(ledger.map(row => row.version));
124
+ const pending = [...migrations].filter(migration => !applied.has(migration.version)).toSorted(byVersion);
125
+ if (pending.length > 0 && conn.transactionalDdl === false) {
126
+ options.onWarning?.(nonTransactionalDdlWarning(conn.dialect));
127
+ }
128
+
129
+ const done: number[] = [];
130
+ for (const migration of pending) {
131
+ const checksum = conn.checksum === undefined ? undefined : await conn.checksum(migration.up);
132
+ try {
133
+ await inTransaction(conn, async transaction => {
134
+ await transaction.exec(migration.up);
135
+ await transaction.recordApplied(migration.version, migration.name, checksum);
136
+ });
137
+ } catch (error) {
138
+ throw migrationFailure('apply', migration, error);
139
+ }
140
+ done.push(migration.version);
141
+ }
142
+ return done;
143
+ }
144
+
145
+ /** Roll back the single most-recently applied migration. */
146
+ export async function down(conn: MigrationConnection, migrations: readonly Migration[]): Promise<number | undefined> {
147
+ assertUniqueVersions(migrations);
148
+ await ensureVersionTable(conn);
149
+ const applied = [...(await verifiedLedger(conn, migrations))].toSorted((left, right) => right.version - left.version);
150
+ const latest = applied[0];
151
+ if (latest === undefined) return undefined;
152
+ const migration = migrations.find(candidate => candidate.version === latest.version);
153
+ if (migration === undefined) throw new Error(`no migration definition for applied version ${String(latest.version)}`);
154
+ if (migration.down.trim().length === 0) {
155
+ throw new Error(`migration ${String(migration.version)} ${migration.name} has no -- zmdb:down section`);
156
+ }
157
+
158
+ try {
159
+ await inTransaction(conn, async transaction => {
160
+ await transaction.exec(migration.down);
161
+ await transaction.recordReverted(latest.version);
162
+ });
163
+ } catch (error) {
164
+ throw migrationFailure('revert', migration, error);
165
+ }
166
+ return latest.version;
167
+ }
168
+
169
+ /** Roll back every applied migration newer than `target`, newest first. */
170
+ export async function downTo(
171
+ conn: MigrationConnection,
172
+ migrations: readonly Migration[],
173
+ target: number,
174
+ ): Promise<number[]> {
175
+ const reverted: number[] = [];
176
+ while (true) {
177
+ const applied = (await status(conn, migrations)).filter(item => item.applied).map(item => item.version);
178
+ const latest = applied.at(-1);
179
+ if (latest === undefined || latest <= target) return reverted;
180
+ const version = await down(conn, migrations);
181
+ if (version === undefined) return reverted;
182
+ reverted.push(version);
183
+ }
184
+ }
185
+
186
+ /** Public lifecycle name for reverting every applied migration above a target version. */
187
+ export const rollbackTo = downTo;
188
+
189
+ export async function status(conn: MigrationConnection, migrations: readonly Migration[]): Promise<MigrationStatus[]> {
190
+ assertUniqueVersions(migrations);
191
+ await ensureVersionTable(conn);
192
+ const applied = new Set((await verifiedLedger(conn, migrations)).map(row => row.version));
193
+ return [...migrations]
194
+ .toSorted(byVersion)
195
+ .map(migration => ({ version: migration.version, name: migration.name, applied: applied.has(migration.version) }));
196
+ }
197
+
198
+ async function verifiedLedger(
199
+ conn: MigrationConnection,
200
+ migrations: readonly Migration[],
201
+ ): Promise<readonly AppliedMigration[]> {
202
+ const rows =
203
+ conn.appliedMigrations === undefined
204
+ ? (await conn.appliedVersions()).map(version => ({ version, name: '', checksum: null }))
205
+ : await conn.appliedMigrations();
206
+ const byMigrationVersion = new Map(migrations.map(migration => [migration.version, migration]));
207
+
208
+ for (const row of rows) {
209
+ if (row.checksum === null) continue;
210
+ const migration = byMigrationVersion.get(row.version);
211
+ if (migration === undefined) continue;
212
+ if (conn.checksum === undefined) {
213
+ throw new Error(
214
+ `migration ${String(row.version)} ${migration.name} has a checksum, but this connection cannot verify it`,
215
+ );
216
+ }
217
+ const actual = await conn.checksum(migration.up);
218
+ if (actual !== row.checksum) {
219
+ throw new Error(
220
+ `migration ${String(row.version)} ${migration.name} was edited after it was applied: ` +
221
+ `ledger has ${row.checksum}, file has ${actual}`,
222
+ );
223
+ }
224
+ }
225
+ return rows;
226
+ }
227
+
228
+ function assertUniqueVersions(migrations: readonly Migration[]): void {
229
+ const seen = new Set<number>();
230
+ for (const migration of migrations) {
231
+ if (!Number.isSafeInteger(migration.version)) {
232
+ throw new TypeError(`migration ${migration.name} version ${String(migration.version)} is not a safe integer`);
233
+ }
234
+ if (seen.has(migration.version)) {
235
+ throw new Error(`duplicate migration version ${String(migration.version)}`);
236
+ }
237
+ seen.add(migration.version);
238
+ }
239
+ }
240
+
241
+ async function inTransaction<T>(
242
+ conn: MigrationConnection,
243
+ run: (connection: MigrationConnection) => Promise<T>,
244
+ ): Promise<T> {
245
+ if (conn.transaction === undefined) return run(conn);
246
+ return conn.transaction(transaction => run(transaction ?? conn));
247
+ }
248
+
249
+ function byVersion(left: Migration, right: Migration): number {
250
+ return left.version - right.version;
251
+ }
252
+
253
+ function migrationFailure(action: 'apply' | 'revert', migration: Migration, error: unknown): Error {
254
+ const sql = action === 'apply' ? migration.up : migration.down;
255
+ const reason = error instanceof Error ? error.message : String(error);
256
+ return new Error(
257
+ `failed to ${action} migration ${String(migration.version)} ${migration.name}: ${reason}\nSQL:\n${sql}`,
258
+ { cause: error },
259
+ );
260
+ }
@@ -0,0 +1,24 @@
1
+ import { cockroach } from '@zmdb/cockroach';
2
+ import { mssql } from '@zmdb/mssql';
3
+ import { mysql } from '@zmdb/mysql';
4
+ import { postgres } from '@zmdb/postgres';
5
+ import { singlestore } from '@zmdb/singlestore';
6
+ import { sqlite } from '@zmdb/sqlite';
7
+
8
+ export const cockroachDialect = cockroach;
9
+ export const mssqlDialect = mssql;
10
+ export const mysqlDialect = mysql;
11
+ export const postgresDialect = postgres;
12
+ export const singlestoreDialect = singlestore;
13
+ export const sqliteDialect = sqlite;
14
+
15
+ export const officialDialects = Object.freeze({
16
+ cockroach: cockroachDialect,
17
+ mssql: mssqlDialect,
18
+ mysql: mysqlDialect,
19
+ postgres: postgresDialect,
20
+ singlestore: singlestoreDialect,
21
+ sqlite: sqliteDialect,
22
+ });
23
+
24
+ export type OfficialDialectName = keyof typeof officialDialects;
package/src/testing.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { AppliedMigration, MigrationConnection } from './runner.js';
2
+
3
+ export interface MemoryMigrationConnection extends MigrationConnection {
4
+ readonly executed: readonly string[];
5
+ readonly ledger: readonly AppliedMigration[];
6
+ }
7
+
8
+ /** Deterministic in-memory protocol for package consumers and dialect conformance suites. */
9
+ export function memoryMigrationConnection(): MemoryMigrationConnection {
10
+ const executed: string[] = [];
11
+ const ledger: AppliedMigration[] = [];
12
+ return {
13
+ executed,
14
+ ledger,
15
+ exec(sql) {
16
+ executed.push(sql);
17
+ },
18
+ appliedVersions() {
19
+ return ledger.map(row => row.version);
20
+ },
21
+ appliedMigrations() {
22
+ return ledger;
23
+ },
24
+ recordApplied(version, name, checksum) {
25
+ ledger.push({ version, name, checksum: checksum ?? null });
26
+ },
27
+ recordReverted(version) {
28
+ const index = ledger.findIndex(row => row.version === version);
29
+ if (index >= 0) ledger.splice(index, 1);
30
+ },
31
+ };
32
+ }