@sqb/migrator 5.0.7 → 6.0.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.
package/README.md
CHANGED
|
@@ -1,23 +1,49 @@
|
|
|
1
|
-
<center>
|
|
2
|
-
<p>
|
|
1
|
+
<p style="text-align:center">
|
|
3
2
|
<img src="https://user-images.githubusercontent.com/3836517/32965280-1a2b63ce-cbe7-11e7-8ee1-ba47313503c5.png" width="500px" alt="SQB Logo"/>
|
|
4
3
|
</p>
|
|
5
|
-
</center>
|
|
6
4
|
|
|
7
5
|
<br>
|
|
8
6
|
|
|
9
7
|
[![NPM Version][npm-image]][npm-url]
|
|
10
8
|
[![NPM Downloads][downloads-image]][downloads-url]
|
|
11
|
-
[![
|
|
9
|
+
[![CI Tests][ci-test-image]][ci-test-url]
|
|
12
10
|
[![Test Coverage][coveralls-image]][coveralls-url]
|
|
13
|
-
[![Dependencies][dependencies-image]][dependencies-url]
|
|
14
|
-
[![DevDependencies][devdependencies-image]][devdependencies-url]
|
|
15
|
-
[![Package Quality][quality-image]][quality-url]
|
|
16
11
|
|
|
17
12
|
## About SQB
|
|
18
13
|
|
|
19
14
|
SQB is an extensible, multi-dialect SQL query builder and Database connection wrapper for NodeJS.
|
|
20
15
|
|
|
16
|
+
## About @sqb/migrator
|
|
17
|
+
|
|
18
|
+
`@sqb/migrator` is a schema/data migration runner for SQB. A `MigrationPackage` describes an
|
|
19
|
+
ordered set of versioned migrations, each made up of one or more tasks:
|
|
20
|
+
|
|
21
|
+
- a raw SQL script (a `.sql` file, or an inline string/function),
|
|
22
|
+
- a data-insert task (`{ tableName, rows }`), or
|
|
23
|
+
- a custom function that runs arbitrary code against the connection.
|
|
24
|
+
|
|
25
|
+
Migrations and tasks can be declared inline or discovered from disk via glob patterns (e.g.
|
|
26
|
+
`v*/migration.json` + `*.task.sql`). `DbMigrator.execute()` loads the package, compares its
|
|
27
|
+
migrations against the target database's tracked version, and applies everything up to an
|
|
28
|
+
optional `targetVersion` in order — recording progress (and per-task success/error events) in a
|
|
29
|
+
`migration_summary` / `migration_events` table pair it creates automatically. Migration scripts
|
|
30
|
+
can reference `$(schema)`, `$(tablespace)` and other variables that get substituted per-run.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { DbMigrator } from '@sqb/migrator';
|
|
34
|
+
|
|
35
|
+
const migrator = new DbMigrator();
|
|
36
|
+
await migrator.execute({
|
|
37
|
+
connection: { dialect: 'postgres', database: 'mydb' },
|
|
38
|
+
migrationPackage: {
|
|
39
|
+
name: 'my-app',
|
|
40
|
+
migrations: ['migrations/v*/migration.json'],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Currently only PostgreSQL is supported, via the bundled `PgMigrationAdapter`.
|
|
46
|
+
|
|
21
47
|
## Main goals
|
|
22
48
|
|
|
23
49
|
- Single code base for any sql based database
|
|
@@ -41,7 +67,7 @@ $ npm install @sqb/migrator --save
|
|
|
41
67
|
|
|
42
68
|
## Node Compatibility
|
|
43
69
|
|
|
44
|
-
- node >=
|
|
70
|
+
- node >= 20.x
|
|
45
71
|
|
|
46
72
|
### License
|
|
47
73
|
|
|
@@ -49,17 +75,9 @@ SQB is available under [MIT](LICENSE) license.
|
|
|
49
75
|
|
|
50
76
|
[npm-image]: https://img.shields.io/npm/v/@sqb/migrator.svg
|
|
51
77
|
[npm-url]: https://npmjs.org/package/@sqb/migrator
|
|
52
|
-
[travis-image]: https://img.shields.io/travis/sqbjs/@sqb/migrator/master.svg
|
|
53
|
-
[travis-url]: https://travis-ci.org/sqbjs/@sqb/migrator
|
|
54
|
-
[coveralls-image]: https://img.shields.io/coveralls/sqbjs/@sqb/migrator/master.svg
|
|
55
|
-
[coveralls-url]: https://coveralls.io/r/sqbjs/@sqb/migrator
|
|
56
78
|
[downloads-image]: https://img.shields.io/npm/dm/@sqb/migrator.svg
|
|
57
79
|
[downloads-url]: https://npmjs.org/package/@sqb/migrator
|
|
58
|
-
[
|
|
59
|
-
[
|
|
60
|
-
[
|
|
61
|
-
[
|
|
62
|
-
[devdependencies-image]: https://david-dm.org/sqbjs/@sqb/migrator/dev-status.svg
|
|
63
|
-
[devdependencies-url]: https://david-dm.org/sqbjs/@sqb/migrator?type=dev
|
|
64
|
-
[quality-image]: http://npm.packagequality.com/shield/@sqb/migrator.png
|
|
65
|
-
[quality-url]: http://packagequality.com/#?package=@sqb/migrator
|
|
80
|
+
[ci-test-image]: https://github.com/panates/sqb/actions/workflows/test.yml/badge.svg
|
|
81
|
+
[ci-test-url]: https://github.com/panates/sqb/actions/workflows/test.yml
|
|
82
|
+
[coveralls-image]: https://coveralls.io/repos/github/sqbjs/sqb/badge.svg?branch=master
|
|
83
|
+
[coveralls-url]: https://coveralls.io/github/sqbjs/sqb?branch=master
|
|
@@ -35,6 +35,11 @@ export declare class PgMigrationAdapter extends MigrationAdapter {
|
|
|
35
35
|
writeEvent(event: MigrationAdapter.Event): Promise<void>;
|
|
36
36
|
executeTask(migrationPackage: MigrationPackage, migration: Migration, task: MigrationTask, variables: Record<string, any>): Promise<void>;
|
|
37
37
|
backupDatabase(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Serializes concurrent migration runs against the same package using a
|
|
40
|
+
* Postgres session-level advisory lock, so two processes migrating the
|
|
41
|
+
* same schema at once can't race on the summary table bookkeeping.
|
|
42
|
+
*/
|
|
38
43
|
lockSchema(): Promise<void>;
|
|
39
44
|
restoreDatabase(): Promise<void>;
|
|
40
45
|
unlockSchema(): Promise<void>;
|
|
@@ -5,6 +5,15 @@ import { MigrationAdapter } from '../migration-adapter.js';
|
|
|
5
5
|
import { isCustomMigrationTask, isInsertDataMigrationTask, isSqlScriptMigrationTask, MigrationPackage, } from '../migration-package.js';
|
|
6
6
|
import { MigrationStatus } from '../types.js';
|
|
7
7
|
const pgAdapter = new PgAdapter();
|
|
8
|
+
// Quotes a (possibly schema-qualified, e.g. "myschema.mytable") Postgres
|
|
9
|
+
// identifier so reserved words / mixed case / special characters in a
|
|
10
|
+
// migration task's tableName or row keys don't break the generated SQL.
|
|
11
|
+
function quoteIdent(name) {
|
|
12
|
+
return name
|
|
13
|
+
.split('.')
|
|
14
|
+
.map(part => '"' + part.replace(/"/g, '""') + '"')
|
|
15
|
+
.join('.');
|
|
16
|
+
}
|
|
8
17
|
export class PgMigrationAdapter extends MigrationAdapter {
|
|
9
18
|
_infoSchema = 'public';
|
|
10
19
|
_version = 0;
|
|
@@ -52,7 +61,10 @@ export class PgMigrationAdapter extends MigrationAdapter {
|
|
|
52
61
|
r.rows?.[0]?.current_schema || 'public';
|
|
53
62
|
}
|
|
54
63
|
// Check if migration schema
|
|
55
|
-
|
|
64
|
+
// No AUTHORIZATION clause - let it default to the connecting role,
|
|
65
|
+
// since hardcoding a specific owner (e.g. "postgres") makes this
|
|
66
|
+
// fail with a permission error for any non-superuser connection.
|
|
67
|
+
await connection.query(`CREATE SCHEMA IF NOT EXISTS ${adapter.infoSchema};`);
|
|
56
68
|
// Create summary table if not exists
|
|
57
69
|
await connection.execute(`
|
|
58
70
|
CREATE TABLE IF NOT EXISTS ${adapter.summaryTableFull}
|
|
@@ -118,7 +130,7 @@ CREATE TABLE IF NOT EXISTS ${adapter.eventTableFull}
|
|
|
118
130
|
params.push(info.status);
|
|
119
131
|
sql += ',\n status = $' + params.length;
|
|
120
132
|
}
|
|
121
|
-
if (info.version && info.version !== this.version) {
|
|
133
|
+
if (info.version != null && info.version !== this.version) {
|
|
122
134
|
params.push(info.version);
|
|
123
135
|
sql += ',\n current_version = $' + params.length;
|
|
124
136
|
}
|
|
@@ -132,7 +144,7 @@ CREATE TABLE IF NOT EXISTS ${adapter.eventTableFull}
|
|
|
132
144
|
await this._connection.query(sql, { params });
|
|
133
145
|
if (info.status)
|
|
134
146
|
this._status = info.status;
|
|
135
|
-
if (info.version)
|
|
147
|
+
if (info.version != null)
|
|
136
148
|
this._version = info.version;
|
|
137
149
|
}
|
|
138
150
|
}
|
|
@@ -207,19 +219,27 @@ CREATE TABLE IF NOT EXISTS ${adapter.eventTableFull}
|
|
|
207
219
|
backupDatabase() {
|
|
208
220
|
return Promise.resolve(undefined);
|
|
209
221
|
}
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Serializes concurrent migration runs against the same package using a
|
|
224
|
+
* Postgres session-level advisory lock, so two processes migrating the
|
|
225
|
+
* same schema at once can't race on the summary table bookkeeping.
|
|
226
|
+
*/
|
|
227
|
+
async lockSchema() {
|
|
228
|
+
await this._connection.query('SELECT pg_advisory_lock(hashtext($1)::bigint)', { params: [this.infoSchema + '.' + this.packageName] });
|
|
229
|
+
// Another process may have advanced the tracked version while we were
|
|
230
|
+
// waiting for the lock - re-read it now that we hold the lock.
|
|
231
|
+
await this.refresh();
|
|
212
232
|
}
|
|
213
233
|
restoreDatabase() {
|
|
214
234
|
return Promise.resolve(undefined);
|
|
215
235
|
}
|
|
216
|
-
unlockSchema() {
|
|
217
|
-
|
|
236
|
+
async unlockSchema() {
|
|
237
|
+
await this._connection.query('SELECT pg_advisory_unlock(hashtext($1)::bigint)', { params: [this.infoSchema + '.' + this.packageName] });
|
|
218
238
|
}
|
|
219
239
|
rowToSql(tableName, row) {
|
|
220
240
|
let sql = '';
|
|
221
241
|
const keys = Object.keys(row);
|
|
222
|
-
sql += `insert into ${tableName} (${keys}) values (`;
|
|
242
|
+
sql += `insert into ${quoteIdent(tableName)} (${keys.map(quoteIdent).join(', ')}) values (`;
|
|
223
243
|
for (let i = 0; i < keys.length; i++) {
|
|
224
244
|
sql += (i ? ', ' : '') + stringifyValueForSQL(row[keys[i]]);
|
|
225
245
|
}
|
package/db-migrator.js
CHANGED
|
@@ -12,7 +12,7 @@ export class DbMigrator extends AsyncEventEmitter {
|
|
|
12
12
|
if (minVersion === Number.MAX_SAFE_INTEGER)
|
|
13
13
|
minVersion = 0;
|
|
14
14
|
const maxVersion = migrationPackage.migrations.reduce((a, m) => Math.max(a, m.version), 0);
|
|
15
|
-
const targetVersion = Math.min(options?.targetVersion
|
|
15
|
+
const targetVersion = Math.min(options?.targetVersion ?? Number.MAX_SAFE_INTEGER, maxVersion);
|
|
16
16
|
if (targetVersion && targetVersion < minVersion) {
|
|
17
17
|
// noinspection ExceptionCaughtLocallyJS
|
|
18
18
|
throw new Error(`Version mismatch. Target schema version (${targetVersion}) is lower than ` +
|
package/migration-package.js
CHANGED
|
@@ -40,6 +40,12 @@ export var MigrationPackage;
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
srcMigrations.sort((a, b) => a.version - b.version);
|
|
43
|
+
const seenVersions = new Set();
|
|
44
|
+
for (const m of srcMigrations) {
|
|
45
|
+
if (seenVersions.has(m.version))
|
|
46
|
+
throw new Error(`Migration package "${asyncConfig.name}" has more than one migration defined for version ${m.version}`);
|
|
47
|
+
seenVersions.add(m.version);
|
|
48
|
+
}
|
|
43
49
|
for (const migration of srcMigrations) {
|
|
44
50
|
const trgMigration = {
|
|
45
51
|
baseDir: '',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqb/migrator",
|
|
3
3
|
"description": "Database migrator for SQB",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.1",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"dependencies": {
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"tslib": "^2.8.1"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@sqb/builder": "^
|
|
16
|
-
"@sqb/connect": "^
|
|
17
|
-
"@sqb/postgres": "^
|
|
15
|
+
"@sqb/builder": "^6.0.1",
|
|
16
|
+
"@sqb/connect": "^6.0.1",
|
|
17
|
+
"@sqb/postgres": "^6.0.1"
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
20
20
|
"module": "./index.js",
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const PATH_PATTERN = /^(?:file:\/\/)?(.+)$/;
|
|
2
2
|
export function getCallingFilename(position = 0) {
|
|
3
3
|
position++;
|
|
4
|
-
|
|
5
|
-
return '';
|
|
4
|
+
const oldStackTraceLimit = Error.stackTraceLimit;
|
|
6
5
|
const oldPrepareStackTrace = Error.prepareStackTrace;
|
|
6
|
+
// Force the limit deep enough for this call regardless of any ambient
|
|
7
|
+
// Error.stackTraceLimit set elsewhere in the process - otherwise a small
|
|
8
|
+
// ambient limit makes this silently return '' and callers fall back to
|
|
9
|
+
// the wrong base directory.
|
|
10
|
+
Error.stackTraceLimit = position + 1;
|
|
7
11
|
Error.prepareStackTrace = (_, stack) => stack;
|
|
8
12
|
const stack = new Error().stack;
|
|
13
|
+
Error.stackTraceLimit = oldStackTraceLimit;
|
|
9
14
|
Error.prepareStackTrace = oldPrepareStackTrace;
|
|
10
15
|
if (stack !== null && typeof stack === 'object') {
|
|
11
16
|
// stack[0] holds this file
|