@deterministic-code/migraters 0.0.0-stage → 0.0.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 (116) hide show
  1. package/README.md +1 -3
  2. package/bundle.yaml +262 -0
  3. package/cli.yaml +264 -0
  4. package/dist/abstractions/migrater.d.ts +28 -0
  5. package/dist/abstractions/migrater.js +1 -0
  6. package/dist/abstractions/sql-dialect-base.d.ts +16 -0
  7. package/dist/abstractions/sql-dialect-base.js +19 -0
  8. package/dist/abstractions/sql-dialect-factory.d.ts +8 -0
  9. package/dist/abstractions/sql-dialect-factory.js +22 -0
  10. package/dist/abstractions/sql-dialect.d.ts +16 -0
  11. package/dist/abstractions/sql-dialect.js +1 -0
  12. package/dist/bin/migrate-create.d.ts +2 -0
  13. package/dist/bin/migrate-create.js +71 -0
  14. package/dist/bin/migrate-down.d.ts +2 -0
  15. package/dist/bin/migrate-down.js +135 -0
  16. package/dist/bin/migrate-init.d.ts +2 -0
  17. package/dist/bin/migrate-init.js +77 -0
  18. package/dist/bin/migrate-setup.d.ts +2 -0
  19. package/dist/bin/migrate-setup.js +87 -0
  20. package/dist/bin/migrate-up.d.ts +2 -0
  21. package/dist/bin/migrate-up.js +97 -0
  22. package/dist/cli-contract.d.ts +6 -0
  23. package/dist/cli-contract.js +19 -0
  24. package/dist/cli-spec.d.ts +24 -0
  25. package/dist/cli-spec.js +33 -0
  26. package/dist/cli.d.ts +9 -0
  27. package/dist/cli.js +41 -0
  28. package/dist/create-sequence.d.ts +4 -0
  29. package/dist/create-sequence.js +16 -0
  30. package/dist/datasource-migrate.d.ts +31 -0
  31. package/dist/datasource-migrate.js +206 -0
  32. package/dist/dialects/mysql-dialect.d.ts +12 -0
  33. package/dist/dialects/mysql-dialect.js +56 -0
  34. package/dist/dialects/oracle-dialect.d.ts +18 -0
  35. package/dist/dialects/oracle-dialect.js +76 -0
  36. package/dist/dialects/postgres-dialect.d.ts +11 -0
  37. package/dist/dialects/postgres-dialect.js +58 -0
  38. package/dist/dialects/sqlite-dialect.d.ts +13 -0
  39. package/dist/dialects/sqlite-dialect.js +96 -0
  40. package/dist/dialects/sqlserver-dialect.d.ts +12 -0
  41. package/dist/dialects/sqlserver-dialect.js +73 -0
  42. package/dist/help.d.ts +2 -0
  43. package/dist/help.js +10 -0
  44. package/dist/index.d.ts +7 -0
  45. package/dist/index.js +6 -0
  46. package/dist/infrastructure/connection-resolver.d.ts +7 -0
  47. package/dist/infrastructure/connection-resolver.js +19 -0
  48. package/dist/infrastructure/default-factory.d.ts +4 -0
  49. package/dist/infrastructure/default-factory.js +15 -0
  50. package/dist/infrastructure/interop-default.d.ts +3 -0
  51. package/dist/infrastructure/interop-default.js +1 -0
  52. package/dist/infrastructure/message-templates.d.ts +3 -0
  53. package/dist/infrastructure/message-templates.js +18 -0
  54. package/dist/infrastructure/split-on-go.d.ts +2 -0
  55. package/dist/infrastructure/split-on-go.js +20 -0
  56. package/dist/infrastructure/sql-templates.d.ts +2 -0
  57. package/dist/infrastructure/sql-templates.js +4 -0
  58. package/dist/infrastructure/templates-root.d.ts +2 -0
  59. package/dist/infrastructure/templates-root.js +11 -0
  60. package/dist/init-select.d.ts +12 -0
  61. package/dist/init-select.js +52 -0
  62. package/dist/migrater.d.ts +12 -0
  63. package/dist/migrater.js +84 -0
  64. package/dist/migration-client.d.ts +21 -0
  65. package/dist/migration-client.js +1 -0
  66. package/dist/path-exists.d.ts +1 -0
  67. package/dist/path-exists.js +10 -0
  68. package/dist/sql.d.ts +3 -0
  69. package/dist/sql.js +15 -0
  70. package/dist/templates/help/create.txt +16 -0
  71. package/dist/templates/help/down.txt +42 -0
  72. package/dist/templates/help/init.txt +13 -0
  73. package/dist/templates/help/setup.txt +41 -0
  74. package/dist/templates/help/up.txt +43 -0
  75. package/dist/templates/messages/checksum-drift-line.txt +1 -0
  76. package/dist/templates/messages/checksum-drift.txt +11 -0
  77. package/dist/templates/messages/down/confirm-mismatch.txt +1 -0
  78. package/dist/templates/messages/down/confirm-prompt.txt +1 -0
  79. package/dist/templates/messages/down/confirm-stdin-failed.txt +1 -0
  80. package/dist/templates/messages/down/confirm-token-mismatch.txt +1 -0
  81. package/dist/templates/messages/down/confirm-tty-required.txt +1 -0
  82. package/dist/templates/messages/down/destructive-header.txt +3 -0
  83. package/dist/templates/messages/errors/invalid-name.txt +1 -0
  84. package/dist/templates/messages/errors/migrate-path-not-dir.txt +1 -0
  85. package/dist/templates/messages/errors/missing-connection.txt +1 -0
  86. package/dist/templates/messages/errors/missing-name-hint.txt +1 -0
  87. package/dist/templates/messages/errors/missing-name.txt +1 -0
  88. package/dist/templates/messages/errors/missing-provider-hint.txt +1 -0
  89. package/dist/templates/messages/errors/missing-provider.txt +1 -0
  90. package/dist/templates/messages/errors/missing-value-for-flag.txt +1 -0
  91. package/dist/templates/messages/errors/rollback-no-down-sibling.txt +1 -0
  92. package/dist/templates/messages/errors/rollback-no-rollback-sibling.txt +1 -0
  93. package/dist/templates/messages/errors/rollback-not-in-path.txt +1 -0
  94. package/dist/templates/messages/errors/sqlite-prerequisite.txt +1 -0
  95. package/dist/templates/messages/errors/unknown-arg.txt +1 -0
  96. package/dist/templates/messages/errors/unsupported-provider.txt +1 -0
  97. package/dist/templates/messages/setup-complete.txt +9 -0
  98. package/dist/templates/messages/status/applied.txt +1 -0
  99. package/dist/templates/messages/status/created.txt +1 -0
  100. package/dist/templates/messages/status/no-more-pending.txt +1 -0
  101. package/dist/templates/messages/status/no-pending.txt +1 -0
  102. package/dist/templates/messages/status/no-rollback.txt +1 -0
  103. package/dist/templates/messages/status/rolled-back.txt +1 -0
  104. package/dist/templates/scaffold/down.sql +1 -0
  105. package/dist/templates/scaffold/up.sql +1 -0
  106. package/dist/templates/sql/mysql/migrate_logs.sql +11 -0
  107. package/dist/templates/sql/mysql/migrates.sql +7 -0
  108. package/dist/templates/sql/oracle/migrate_logs.sql +14 -0
  109. package/dist/templates/sql/oracle/migrates.sql +10 -0
  110. package/dist/templates/sql/postgres/migrate_logs.sql +11 -0
  111. package/dist/templates/sql/postgres/migrates.sql +7 -0
  112. package/dist/templates/sql/sqlite/migrate_logs.sql +11 -0
  113. package/dist/templates/sql/sqlite/migrates.sql +7 -0
  114. package/dist/templates/sql/sqlserver/migrate_logs.sql +12 -0
  115. package/dist/templates/sql/sqlserver/migrates.sql +8 -0
  116. package/package.json +79 -4
@@ -0,0 +1,206 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { defaultDialectFactory } from "./infrastructure/default-factory.js";
5
+ import { splitOnGo } from "./infrastructure/split-on-go.js";
6
+ import { fillTemplate, loadMessage } from "./infrastructure/message-templates.js";
7
+ export { normalizeDialect } from "./sql.js";
8
+ export { applyMysqlDdlViaTextProtocol } from "./dialects/mysql-dialect.js";
9
+ const UP_SUFFIX = '_up.sql';
10
+ const DOWN_SUFFIX = '_down.sql';
11
+ const [checksumDriftTpl, checksumDriftLineTpl, rollbackNotInPathMsg, rollbackNoSiblingTpl, migratePathNotDirTpl,] = await Promise.all([
12
+ loadMessage('checksum-drift'),
13
+ loadMessage('checksum-drift-line'),
14
+ loadMessage('errors/rollback-not-in-path'),
15
+ loadMessage('errors/rollback-no-rollback-sibling'),
16
+ loadMessage('errors/migrate-path-not-dir'),
17
+ ]);
18
+ const missingMigrationsDir = (path) => new Error(fillTemplate(migratePathNotDirTpl, { path }).trimEnd());
19
+ export async function discoverMigrations({ migratePath, dialect, }) {
20
+ requireDialect(dialect);
21
+ let st;
22
+ try {
23
+ st = await stat(migratePath);
24
+ }
25
+ catch {
26
+ throw missingMigrationsDir(migratePath);
27
+ }
28
+ if (!st.isDirectory())
29
+ throw missingMigrationsDir(migratePath);
30
+ const entries = await readdir(migratePath, { withFileTypes: true });
31
+ const files = new Set(entries.filter((e) => e.isFile()).map((e) => e.name));
32
+ const ups = [];
33
+ for (const name of files) {
34
+ if (!name.endsWith(UP_SUFFIX))
35
+ continue;
36
+ const stem = name.slice(0, -UP_SUFFIX.length);
37
+ const downName = `${stem}${DOWN_SUFFIX}`;
38
+ ups.push({
39
+ name: stem,
40
+ upPath: join(migratePath, name),
41
+ downPath: files.has(downName) ? join(migratePath, downName) : null,
42
+ });
43
+ }
44
+ ups.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
45
+ return ups;
46
+ }
47
+ /** Strip pre-refactor `-- deterministic-content-hash: <sha>` header if present so upgrading users carrying the legacy header keep the same checksum migrate:up recorded. */
48
+ const LEGACY_HEADER_RE = /^(?:\/\/|--|#)\s+deterministic-content-hash:\s+[0-9a-f]{64}\s*\r?\n?/;
49
+ export function checksum(text) {
50
+ const body = text.replace(LEGACY_HEADER_RE, '');
51
+ return createHash('sha256').update(body, 'utf8').digest('hex');
52
+ }
53
+ export async function applyOneUp({ client, migrations, env = process.env, }) {
54
+ const applied = await appliedRecords(client);
55
+ await assertNoChecksumDrift({ migrations, applied, env });
56
+ const next = migrations.find((m) => !applied.has(m.name));
57
+ if (!next)
58
+ return { applied: false, name: null };
59
+ const sqlText = await readFile(next.upPath, 'utf8');
60
+ const sum = checksum(sqlText);
61
+ const logId = await insertLogStarted(client, next.name, 'up');
62
+ const startedAt = Date.now();
63
+ try {
64
+ await client.transaction(async () => {
65
+ const stmts = splitOnGo(await readFile(next.upPath, 'utf8'));
66
+ for (const stmt of stmts) {
67
+ await client.exec(stmt);
68
+ }
69
+ await insertMigrate(client, next.name, sum);
70
+ });
71
+ }
72
+ catch (e) {
73
+ const err = e;
74
+ await updateLogTerminal({
75
+ client,
76
+ id: logId,
77
+ status: 'error',
78
+ durationMs: Date.now() - startedAt,
79
+ errorMessage: String(err?.message ?? e),
80
+ });
81
+ throw e;
82
+ }
83
+ await updateLogTerminal({
84
+ client,
85
+ id: logId,
86
+ status: 'success',
87
+ durationMs: Date.now() - startedAt,
88
+ errorMessage: null,
89
+ });
90
+ return { applied: true, name: next.name };
91
+ }
92
+ export async function applyOneDown({ client, migrations, env = process.env, }) {
93
+ const applied = await appliedRecords(client);
94
+ await assertNoChecksumDrift({ migrations, applied, env });
95
+ if (applied.size === 0)
96
+ return { rolledBack: false, name: null };
97
+ const lastApplied = [...migrations].reverse().find((m) => applied.has(m.name));
98
+ if (!lastApplied) {
99
+ throw new Error(rollbackNotInPathMsg.trimEnd());
100
+ }
101
+ if (!lastApplied.downPath) {
102
+ throw new Error(fillTemplate(rollbackNoSiblingTpl, { name: lastApplied.name }).trimEnd());
103
+ }
104
+ const downPath = lastApplied.downPath;
105
+ const logId = await insertLogStarted(client, lastApplied.name, 'down');
106
+ const startedAt = Date.now();
107
+ try {
108
+ await client.transaction(async () => {
109
+ const stmts = splitOnGo(await readFile(downPath, 'utf8'));
110
+ for (const stmt of stmts) {
111
+ await client.exec(stmt);
112
+ }
113
+ await deleteMigrate(client, lastApplied.name);
114
+ });
115
+ }
116
+ catch (e) {
117
+ const err = e;
118
+ await updateLogTerminal({
119
+ client,
120
+ id: logId,
121
+ status: 'error',
122
+ durationMs: Date.now() - startedAt,
123
+ errorMessage: String(err?.message ?? e),
124
+ });
125
+ throw e;
126
+ }
127
+ await updateLogTerminal({
128
+ client,
129
+ id: logId,
130
+ status: 'success',
131
+ durationMs: Date.now() - startedAt,
132
+ errorMessage: null,
133
+ });
134
+ return { rolledBack: true, name: lastApplied.name };
135
+ }
136
+ function dialectOf(client) {
137
+ return defaultDialectFactory.get(client.dialect);
138
+ }
139
+ async function appliedRecords(client) {
140
+ const d = dialectOf(client);
141
+ const q = (ident) => d.quoteIdent(ident);
142
+ const rows = await client.query(`SELECT ${q('name')} AS name, ${q('checksum')} AS checksum FROM ${q('migrates')}`);
143
+ return new Map(rows.map((r) => [r.name ?? r.NAME, r.checksum ?? r.CHECKSUM ?? null]));
144
+ }
145
+ async function assertNoChecksumDrift({ migrations, applied, env = process.env, }) {
146
+ if (env.MIGRATE_ALLOW_CHECKSUM_DRIFT === '1')
147
+ return;
148
+ const drifted = [];
149
+ for (const m of migrations) {
150
+ if (!applied.has(m.name))
151
+ continue;
152
+ const stored = applied.get(m.name);
153
+ if (stored == null)
154
+ continue;
155
+ const current = checksum(await readFile(m.upPath, 'utf8'));
156
+ if (current !== stored)
157
+ drifted.push({ name: m.name, stored, current });
158
+ }
159
+ if (drifted.length === 0)
160
+ return;
161
+ const lines = drifted
162
+ .map((d) => fillTemplate(checksumDriftLineTpl, {
163
+ name: d.name,
164
+ stored: d.stored.slice(0, 12),
165
+ current: d.current.slice(0, 12),
166
+ }).trimEnd())
167
+ .join('\n');
168
+ throw new Error(fillTemplate(checksumDriftTpl, { lines }).trimEnd());
169
+ }
170
+ async function insertMigrate(client, name, sum) {
171
+ const d = dialectOf(client);
172
+ const t = d.quoteIdent('migrates');
173
+ await client.run(`INSERT INTO ${t} (${d.quoteIdent('name')}, ${d.quoteIdent('checksum')}) VALUES (?, ?)`, [name, sum]);
174
+ }
175
+ async function deleteMigrate(client, name) {
176
+ const d = dialectOf(client);
177
+ const t = d.quoteIdent('migrates');
178
+ await client.run(`DELETE FROM ${t} WHERE ${d.quoteIdent('name')} = ?`, [name]);
179
+ }
180
+ async function insertLogStarted(client, name, direction) {
181
+ const d = dialectOf(client);
182
+ const t = d.quoteIdent('migrate_logs');
183
+ if (d.usesLastInsertRowid) {
184
+ const stmt = `INSERT INTO ${t} (${d.quoteIdent('migrate_name')}, ${d.quoteIdent('direction')}, ${d.quoteIdent('status')}) VALUES (?, ?, 'started')`;
185
+ const db = client._raw;
186
+ if (db) {
187
+ const info = db.prepare(stmt).run(name, direction);
188
+ return info.lastInsertRowid;
189
+ }
190
+ await client.run(stmt, [name, direction]);
191
+ const rows = await client.query(`SELECT ${d.quoteIdent('id')} AS id FROM ${t} WHERE ${d.quoteIdent('migrate_name')} = ? AND ${d.quoteIdent('direction')} = ? ORDER BY id DESC ${d.limitClause(1)}`, [name, direction]);
192
+ return rows[0]?.id;
193
+ }
194
+ await client.run(`INSERT INTO ${t} (${d.quoteIdent('migrate_name')}, ${d.quoteIdent('direction')}, ${d.quoteIdent('status')}) VALUES (?, ?, 'started')`, [name, direction]);
195
+ const rows = await client.query(`SELECT ${d.quoteIdent('id')} AS id FROM ${t} WHERE ${d.quoteIdent('migrate_name')} = ? AND ${d.quoteIdent('direction')} = ? ORDER BY ${d.quoteIdent('id')} DESC ${d.limitClause(1)}`, [name, direction]);
196
+ return rows[0]?.id ?? rows[0]?.ID;
197
+ }
198
+ async function updateLogTerminal({ client, id, status, durationMs, errorMessage, }) {
199
+ const d = dialectOf(client);
200
+ const t = d.quoteIdent('migrate_logs');
201
+ const setNow = d.nowExpr();
202
+ await client.run(`UPDATE ${t} SET ${d.quoteIdent('status')} = ?, ${d.quoteIdent('finished_at')} = ${setNow}, ${d.quoteIdent('duration_ms')} = ?, ${d.quoteIdent('error_message')} = ?, ${d.quoteIdent('updated')} = ${setNow} WHERE ${d.quoteIdent('id')} = ?`, [status, durationMs, errorMessage, id]);
203
+ }
204
+ function requireDialect(dialect) {
205
+ return defaultDialectFactory.get(dialect).name;
206
+ }
@@ -0,0 +1,12 @@
1
+ import type { Connection as MysqlConnection } from 'mysql2/promise';
2
+ import type { MigrationClient } from '../migration-client.ts';
3
+ import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
4
+ export declare function applyMysqlDdlViaTextProtocol(conn: Pick<MysqlConnection, 'query'>, sql: string): Promise<void>;
5
+ export declare class MysqlDialect extends SqlDialectBase {
6
+ readonly name: "mysql";
7
+ readonly connectionEnvironmentVariables: readonly ["MYSQL_URL", "DATABASE_URL"];
8
+ readonly migratesDdl: string;
9
+ readonly migrateLogsDdl: string;
10
+ quoteIdent(ident: string): string;
11
+ createClient(connection: string): Promise<MigrationClient>;
12
+ }
@@ -0,0 +1,56 @@
1
+ import { readSqlTemplate } from "../infrastructure/sql-templates.js";
2
+ import { SqlDialectBase } from "../abstractions/sql-dialect-base.js";
3
+ const [migratesDdl, migrateLogsDdl] = await Promise.all([
4
+ readSqlTemplate('mysql', 'migrates'),
5
+ readSqlTemplate('mysql', 'migrate_logs'),
6
+ ]);
7
+ export async function applyMysqlDdlViaTextProtocol(conn, sql) {
8
+ // why: DDL statements (DROP/CREATE PROCEDURE) reject prepared-statement protocol (MySQL 1295)
9
+ await conn.query(sql);
10
+ }
11
+ export class MysqlDialect extends SqlDialectBase {
12
+ name = 'mysql';
13
+ connectionEnvironmentVariables = ['MYSQL_URL', 'DATABASE_URL'];
14
+ migratesDdl = migratesDdl;
15
+ migrateLogsDdl = migrateLogsDdl;
16
+ quoteIdent(ident) {
17
+ return `\`${ident}\``;
18
+ }
19
+ async createClient(connection) {
20
+ const mysql = await import('mysql2/promise');
21
+ const conn = await mysql.createConnection(connection);
22
+ return {
23
+ dialect: 'mysql',
24
+ async exec(sql) {
25
+ await applyMysqlDdlViaTextProtocol(conn, sql);
26
+ },
27
+ async query(sql, params = []) {
28
+ const [rows] = await conn.execute(sql, params);
29
+ return rows;
30
+ },
31
+ async run(sql, params = []) {
32
+ await conn.execute(sql, params);
33
+ },
34
+ async transaction(fn) {
35
+ await conn.beginTransaction();
36
+ try {
37
+ const r = await fn();
38
+ await conn.commit();
39
+ return r;
40
+ }
41
+ catch (e) {
42
+ try {
43
+ await conn.rollback();
44
+ }
45
+ catch (rollbackErr) {
46
+ console.warn('mysql rollback failed after transaction error; original error rethrown', rollbackErr);
47
+ }
48
+ throw e;
49
+ }
50
+ },
51
+ async close() {
52
+ await conn.end();
53
+ },
54
+ };
55
+ }
56
+ }
@@ -0,0 +1,18 @@
1
+ import type { MigrationClient } from '../migration-client.ts';
2
+ import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
3
+ interface OracleConnectionConfig {
4
+ user: string;
5
+ password: string;
6
+ connectString: string;
7
+ }
8
+ export declare const parseOracleConnection: (connection: string | OracleConnectionConfig) => OracleConnectionConfig;
9
+ export declare class OracleDialect extends SqlDialectBase {
10
+ readonly name: "oracle";
11
+ readonly connectionEnvironmentVariables: readonly ["ORACLE_CONNECT_STRING", "DATABASE_URL"];
12
+ readonly migratesDdl: string;
13
+ readonly migrateLogsDdl: string;
14
+ translatePlaceholders(sql: string): string;
15
+ limitClause(n: number): string;
16
+ createClient(connection: string): Promise<MigrationClient>;
17
+ }
18
+ export {};
@@ -0,0 +1,76 @@
1
+ import { interopDefault } from "../infrastructure/interop-default.js";
2
+ import { readSqlTemplate } from "../infrastructure/sql-templates.js";
3
+ import { SqlDialectBase } from "../abstractions/sql-dialect-base.js";
4
+ const [migratesDdl, migrateLogsDdl] = await Promise.all([
5
+ readSqlTemplate('oracle', 'migrates'),
6
+ readSqlTemplate('oracle', 'migrate_logs'),
7
+ ]);
8
+ export const parseOracleConnection = (connection) => {
9
+ if (typeof connection === 'object' && connection)
10
+ return connection;
11
+ const m = /^([^/]+)\/([^@]+)@(.+)$/.exec(String(connection));
12
+ if (!m) {
13
+ throw new Error(`Oracle connection must look like user/password@connectString`);
14
+ }
15
+ return { user: m[1], password: m[2], connectString: m[3] };
16
+ };
17
+ const lowercaseKeys = (row) => {
18
+ const out = {};
19
+ for (const [k, v] of Object.entries(row))
20
+ out[k.toLowerCase()] = v;
21
+ return out;
22
+ };
23
+ export class OracleDialect extends SqlDialectBase {
24
+ name = 'oracle';
25
+ connectionEnvironmentVariables = ['ORACLE_CONNECT_STRING', 'DATABASE_URL'];
26
+ migratesDdl = migratesDdl;
27
+ migrateLogsDdl = migrateLogsDdl;
28
+ translatePlaceholders(sql) {
29
+ let n = 0;
30
+ return sql.replace(/\?/g, () => `:${++n}`);
31
+ }
32
+ limitClause(n) {
33
+ return `FETCH FIRST ${n} ROWS ONLY`;
34
+ }
35
+ async createClient(connection) {
36
+ const oracledb = interopDefault(await import('oracledb'));
37
+ const conn = await oracledb.getConnection(parseOracleConnection(connection));
38
+ const translate = (sql) => this.translatePlaceholders(sql);
39
+ return {
40
+ dialect: 'oracle',
41
+ async exec(sql) {
42
+ await conn.execute(sql, [], { autoCommit: true });
43
+ },
44
+ async query(sql, params = []) {
45
+ const r = await conn.execute(translate(sql), params, {
46
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
47
+ });
48
+ return (r.rows ?? []).map(lowercaseKeys);
49
+ },
50
+ async run(sql, params = []) {
51
+ await conn.execute(translate(sql), params, {
52
+ autoCommit: false,
53
+ });
54
+ },
55
+ async transaction(fn) {
56
+ try {
57
+ const r = await fn();
58
+ await conn.commit();
59
+ return r;
60
+ }
61
+ catch (e) {
62
+ try {
63
+ await conn.rollback();
64
+ }
65
+ catch (rollbackErr) {
66
+ console.warn('oracle rollback failed after transaction error; original error rethrown', rollbackErr);
67
+ }
68
+ throw e;
69
+ }
70
+ },
71
+ async close() {
72
+ await conn.close();
73
+ },
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,11 @@
1
+ import type { MigrationClient } from '../migration-client.ts';
2
+ import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
3
+ export declare class PostgresDialect extends SqlDialectBase {
4
+ readonly name: "postgres";
5
+ readonly connectionEnvironmentVariables: readonly ["PG_CONNECTION_STRING", "DATABASE_URL"];
6
+ readonly migratesDdl: string;
7
+ readonly migrateLogsDdl: string;
8
+ nowExpr(): string;
9
+ translatePlaceholders(sql: string): string;
10
+ createClient(connection: string): Promise<MigrationClient>;
11
+ }
@@ -0,0 +1,58 @@
1
+ import { readSqlTemplate } from "../infrastructure/sql-templates.js";
2
+ import { SqlDialectBase } from "../abstractions/sql-dialect-base.js";
3
+ const [migratesDdl, migrateLogsDdl] = await Promise.all([
4
+ readSqlTemplate('postgres', 'migrates'),
5
+ readSqlTemplate('postgres', 'migrate_logs'),
6
+ ]);
7
+ export class PostgresDialect extends SqlDialectBase {
8
+ name = 'postgres';
9
+ connectionEnvironmentVariables = ['PG_CONNECTION_STRING', 'DATABASE_URL'];
10
+ migratesDdl = migratesDdl;
11
+ migrateLogsDdl = migrateLogsDdl;
12
+ nowExpr() {
13
+ return 'NOW()';
14
+ }
15
+ translatePlaceholders(sql) {
16
+ let n = 0;
17
+ return sql.replace(/\?/g, () => `$${++n}`);
18
+ }
19
+ async createClient(connection) {
20
+ const { default: pg } = await import('pg');
21
+ const client = new pg.Client({ connectionString: connection });
22
+ await client.connect();
23
+ const translate = (sql) => this.translatePlaceholders(sql);
24
+ return {
25
+ dialect: 'postgres',
26
+ async exec(sql) {
27
+ await client.query(sql);
28
+ },
29
+ async query(sql, params = []) {
30
+ const r = await client.query(translate(sql), params);
31
+ return r.rows;
32
+ },
33
+ async run(sql, params = []) {
34
+ await client.query(translate(sql), params);
35
+ },
36
+ async transaction(fn) {
37
+ await client.query('BEGIN');
38
+ try {
39
+ const r = await fn();
40
+ await client.query('COMMIT');
41
+ return r;
42
+ }
43
+ catch (e) {
44
+ try {
45
+ await client.query('ROLLBACK');
46
+ }
47
+ catch (rollbackErr) {
48
+ console.warn('postgres ROLLBACK failed after transaction error; original error rethrown', rollbackErr);
49
+ }
50
+ throw e;
51
+ }
52
+ },
53
+ async close() {
54
+ await client.end();
55
+ },
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,13 @@
1
+ import type { MigrationClient } from '../migration-client.ts';
2
+ import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
3
+ export declare const sqliteFilesystemPath: (connection: string) => string | null;
4
+ export declare class SqliteDialect extends SqlDialectBase {
5
+ readonly name: "sqlite";
6
+ readonly connectionEnvironmentVariables: readonly ["SQLITE_PATH", "DB_PATH"];
7
+ readonly migratesDdl: string;
8
+ readonly migrateLogsDdl: string;
9
+ readonly usesLastInsertRowid = true;
10
+ prerequisiteError(connection: string): Promise<string | null>;
11
+ prepareSetup(connection: string): Promise<void>;
12
+ createClient(connection: string): Promise<MigrationClient>;
13
+ }
@@ -0,0 +1,96 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { pathExists } from "../path-exists.js";
4
+ import { readSqlTemplate } from "../infrastructure/sql-templates.js";
5
+ import { fillTemplate, loadMessage } from "../infrastructure/message-templates.js";
6
+ import { SqlDialectBase } from "../abstractions/sql-dialect-base.js";
7
+ const [migratesDdl, migrateLogsDdl, sqlitePrerequisiteTpl] = await Promise.all([
8
+ readSqlTemplate('sqlite', 'migrates'),
9
+ readSqlTemplate('sqlite', 'migrate_logs'),
10
+ loadMessage('errors/sqlite-prerequisite'),
11
+ ]);
12
+ export const sqliteFilesystemPath = (connection) => {
13
+ let s = connection.trim();
14
+ const eq = s.indexOf('=');
15
+ if (eq >= 0 && /data\s*source/i.test(s.slice(0, eq))) {
16
+ s = s.slice(eq + 1).trim();
17
+ }
18
+ else if (/^sqlite:\/\//i.test(s)) {
19
+ s = s.slice('sqlite://'.length);
20
+ }
21
+ else if (/^sqlite:/i.test(s)) {
22
+ s = s.slice('sqlite:'.length);
23
+ }
24
+ else if (/^file:/i.test(s)) {
25
+ s = s.slice('file:'.length);
26
+ }
27
+ if (s === ':memory:' || s === '')
28
+ return null;
29
+ return s;
30
+ };
31
+ export class SqliteDialect extends SqlDialectBase {
32
+ name = 'sqlite';
33
+ connectionEnvironmentVariables = ['SQLITE_PATH', 'DB_PATH'];
34
+ migratesDdl = migratesDdl;
35
+ migrateLogsDdl = migrateLogsDdl;
36
+ usesLastInsertRowid = true;
37
+ async prerequisiteError(connection) {
38
+ const path = sqliteFilesystemPath(connection);
39
+ if (path === null)
40
+ return null;
41
+ if (await pathExists(path))
42
+ return null;
43
+ return fillTemplate(sqlitePrerequisiteTpl, { path });
44
+ }
45
+ async prepareSetup(connection) {
46
+ const filePath = sqliteFilesystemPath(connection);
47
+ if (filePath !== null) {
48
+ await mkdir(dirname(filePath), { recursive: true });
49
+ }
50
+ }
51
+ async createClient(connection) {
52
+ if (!connection)
53
+ throw new Error('sqlite requires a database file path');
54
+ const { default: Database } = await import('better-sqlite3');
55
+ const db = new Database(connection);
56
+ return {
57
+ dialect: 'sqlite',
58
+ async exec(sql) {
59
+ db.exec(sql);
60
+ },
61
+ async query(sql, params = []) {
62
+ const stmt = db.prepare(sql);
63
+ try {
64
+ return stmt.all(...params);
65
+ }
66
+ catch {
67
+ stmt.run(...params);
68
+ return [];
69
+ }
70
+ },
71
+ async run(sql, params = []) {
72
+ db.prepare(sql).run(...params);
73
+ },
74
+ async transaction(fn) {
75
+ db.exec('BEGIN');
76
+ try {
77
+ const r = await fn();
78
+ db.exec('COMMIT');
79
+ return r;
80
+ }
81
+ catch (e) {
82
+ try {
83
+ db.exec('ROLLBACK');
84
+ }
85
+ catch (rollbackErr) {
86
+ console.warn('sqlite ROLLBACK failed after transaction error; original error rethrown', rollbackErr);
87
+ }
88
+ throw e;
89
+ }
90
+ },
91
+ async close() {
92
+ db.close();
93
+ },
94
+ };
95
+ }
96
+ }
@@ -0,0 +1,12 @@
1
+ import type { MigrationClient } from '../migration-client.ts';
2
+ import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
3
+ export declare class SqlServerDialect extends SqlDialectBase {
4
+ readonly name: "sqlserver";
5
+ readonly connectionEnvironmentVariables: readonly ["MSSQL_URL", "DATABASE_URL"];
6
+ readonly migratesDdl: string;
7
+ readonly migrateLogsDdl: string;
8
+ quoteIdent(ident: string): string;
9
+ nowExpr(): string;
10
+ limitClause(n: number): string;
11
+ createClient(connection: string): Promise<MigrationClient>;
12
+ }
@@ -0,0 +1,73 @@
1
+ import { interopDefault } from "../infrastructure/interop-default.js";
2
+ import { readSqlTemplate } from "../infrastructure/sql-templates.js";
3
+ import { SqlDialectBase } from "../abstractions/sql-dialect-base.js";
4
+ const [migratesDdl, migrateLogsDdl] = await Promise.all([
5
+ readSqlTemplate('sqlserver', 'migrates'),
6
+ readSqlTemplate('sqlserver', 'migrate_logs'),
7
+ ]);
8
+ const bindMssql = (req, sql, params) => {
9
+ let n = 0;
10
+ return sql.replace(/\?/g, () => {
11
+ const name = `p${n}`;
12
+ req.input(name, params[n]);
13
+ n++;
14
+ return `@${name}`;
15
+ });
16
+ };
17
+ export class SqlServerDialect extends SqlDialectBase {
18
+ name = 'sqlserver';
19
+ connectionEnvironmentVariables = ['MSSQL_URL', 'DATABASE_URL'];
20
+ migratesDdl = migratesDdl;
21
+ migrateLogsDdl = migrateLogsDdl;
22
+ quoteIdent(ident) {
23
+ return `[${ident}]`;
24
+ }
25
+ nowExpr() {
26
+ return 'SYSUTCDATETIME()';
27
+ }
28
+ limitClause(n) {
29
+ return `OFFSET 0 ROWS FETCH NEXT ${n} ROWS ONLY`;
30
+ }
31
+ async createClient(connection) {
32
+ const mssql = interopDefault(await import('mssql'));
33
+ const pool = await mssql.connect(connection);
34
+ return {
35
+ dialect: 'sqlserver',
36
+ async exec(sql) {
37
+ await pool.request().batch(sql);
38
+ },
39
+ async query(sql, params = []) {
40
+ const req = pool.request();
41
+ const translated = bindMssql(req, sql, params);
42
+ const r = await req.query(translated);
43
+ return r.recordset ?? [];
44
+ },
45
+ async run(sql, params = []) {
46
+ const req = pool.request();
47
+ const translated = bindMssql(req, sql, params);
48
+ await req.query(translated);
49
+ },
50
+ async transaction(fn) {
51
+ const tx = pool.transaction();
52
+ await tx.begin();
53
+ try {
54
+ const r = await fn();
55
+ await tx.commit();
56
+ return r;
57
+ }
58
+ catch (e) {
59
+ try {
60
+ await tx.rollback();
61
+ }
62
+ catch (rollbackErr) {
63
+ console.warn('sqlserver rollback failed after transaction error; original error rethrown', rollbackErr);
64
+ }
65
+ throw e;
66
+ }
67
+ },
68
+ async close() {
69
+ await pool.close();
70
+ },
71
+ };
72
+ }
73
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const loadHelp: (command: string) => Promise<string>;
2
+ export declare const loadProgramUsage: () => Promise<string>;
package/dist/help.js ADDED
@@ -0,0 +1,10 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { fillHelpTemplate, programUsageText, verbFromCommand } from "./cli-contract.js";
4
+ import { templatesRoot } from "./infrastructure/templates-root.js";
5
+ export const loadHelp = async (command) => {
6
+ const verb = verbFromCommand(command);
7
+ const text = await readFile(join(templatesRoot(), 'help', `${verb}.txt`), 'utf8');
8
+ return fillHelpTemplate(text, command);
9
+ };
10
+ export const loadProgramUsage = async () => programUsageText();
@@ -0,0 +1,7 @@
1
+ export * from './datasource-migrate.ts';
2
+ export type { CreateArg, CreateResponse, DownArg, IMigrater, SetupArg, UpArg, } from './abstractions/migrater.ts';
3
+ export { getMigrater, Migrater } from './migrater.ts';
4
+ export { fillHelpTemplate, migrateCommand, verbFromCommand, type MigrateVerb, } from './cli-contract.ts';
5
+ export { loadHelp, loadProgramUsage } from './help.ts';
6
+ export { fillTemplate, loadMessage, loadScaffold, } from './infrastructure/message-templates.ts';
7
+ export { normalizeDialect, pathExists, type SqlDialect } from './sql.ts';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./datasource-migrate.js";
2
+ export { getMigrater, Migrater } from "./migrater.js";
3
+ export { fillHelpTemplate, migrateCommand, verbFromCommand, } from "./cli-contract.js";
4
+ export { loadHelp, loadProgramUsage } from "./help.js";
5
+ export { fillTemplate, loadMessage, loadScaffold, } from "./infrastructure/message-templates.js";
6
+ export { normalizeDialect, pathExists } from "./sql.js";