@sebspark/spanner-migrate 0.1.1 → 1.0.0
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 +155 -49
- package/dist/{chunk-K5WX6ESL.mjs → chunk-SZDH364K.mjs} +94 -60
- package/dist/cli.js +254 -98
- package/dist/cli.mjs +160 -38
- package/dist/index.d.mts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +93 -59
- package/dist/index.mjs +1 -1
- package/package.json +14 -9
package/README.md
CHANGED
|
@@ -14,90 +14,196 @@ yarn add -D @sebspark/spanner-migrate
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## CLI
|
|
17
|
+
## CLI Commands
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
`spanner-migrate` provides several commands for managing database migrations in Google Spanner.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
### Initialize Configuration
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
spanner-migrate init
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Initializes a `.spanner-migrate.config.json` file by prompting for:
|
|
28
|
+
- Spanner instance name
|
|
29
|
+
- One or more database configurations
|
|
30
|
+
- Optional Google Cloud project name
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
Initialize a Spanner migration configuration file (`.spanner-migrate.config.json`).
|
|
32
|
+
### Create a Migration
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
```sh
|
|
35
|
+
spanner-migrate create <description ...> [--database <name>]
|
|
36
|
+
spanner-migrate create add users table
|
|
37
|
+
spanner-migrate create --database=mydb add users table
|
|
38
|
+
```
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
Creates a new migration file with the specified description.
|
|
41
|
+
|
|
42
|
+
- If `--database` (`-d`) is provided, it uses the specified database.
|
|
43
|
+
- If multiple databases exist and none is specified, the user is prompted to select one.
|
|
44
|
+
- The filename is generated from the description (`<timestamp>_add_users_table.sql`).
|
|
45
|
+
|
|
46
|
+
### Apply Migrations
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
spanner-migrate up
|
|
50
|
+
spanner-migrate up --database <name>
|
|
51
|
+
spanner-migrate up --database <name> --max <n>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Applies pending migrations.
|
|
55
|
+
|
|
56
|
+
- If **no** `--database` and `--max` are provided, applies all migrations to all databases.
|
|
57
|
+
- If `--database` (`-d`) is provided, applies migrations only to that database.
|
|
58
|
+
- If `--max` (`-m`) is provided, limits the number of migrations applied (requires `--database`).
|
|
59
|
+
- `--max` must be an integer greater than 0.
|
|
60
|
+
|
|
61
|
+
### Roll Back Last Migration
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
spanner-migrate down
|
|
65
|
+
spanner-migrate down --database <name>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Rolls back the last applied migration.
|
|
69
|
+
|
|
70
|
+
- If a **single** database exists, it is automatically selected.
|
|
71
|
+
- If multiple databases exist, `--database` is **required**.
|
|
72
|
+
- The specified `--database` must exist.
|
|
73
|
+
|
|
74
|
+
### Show Migration Status
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
spanner-migrate status
|
|
78
|
+
spanner-migrate status --database <name>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Displays migration status.
|
|
82
|
+
|
|
83
|
+
- If `--database` is specified, shows status for that database.
|
|
84
|
+
- If no `--database` is provided, shows status for all configured databases.
|
|
85
|
+
|
|
86
|
+
### Help
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
spanner-migrate --help
|
|
90
|
+
spanner-migrate <command> --help
|
|
34
91
|
```
|
|
35
92
|
|
|
36
|
-
|
|
37
|
-
- `Enter the path for your migrations`: Directory for migration files (default: `./migrations`).
|
|
38
|
-
- `Enter Spanner instance name`: The name of the Spanner instance.
|
|
39
|
-
- `Enter Spanner database name`: The name of the Spanner database.
|
|
40
|
-
- `Enter Google Cloud project name`: (Optional) The Google Cloud project name.
|
|
93
|
+
Displays help for the CLI or a specific command.
|
|
41
94
|
|
|
42
95
|
---
|
|
43
96
|
|
|
44
|
-
|
|
45
|
-
Create a new migration file.
|
|
97
|
+
## Programmatic Usage
|
|
46
98
|
|
|
47
|
-
|
|
99
|
+
In addition to the CLI, `spanner-migrate` can be used as a Node.js module to manage migrations programmatically.
|
|
48
100
|
|
|
49
|
-
|
|
50
|
-
|
|
101
|
+
### Importing
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { init, create, up, down, status } from '@sebspark/spanner-migrate'
|
|
51
105
|
```
|
|
52
106
|
|
|
53
|
-
|
|
54
|
-
Apply pending migrations
|
|
107
|
+
### Initializing Configuration
|
|
55
108
|
|
|
56
|
-
|
|
109
|
+
```typescript
|
|
110
|
+
import { init, type Config } from '@sebspark/spanner-migrate'
|
|
57
111
|
|
|
58
|
-
|
|
59
|
-
|
|
112
|
+
const config: Config = {
|
|
113
|
+
instance: {
|
|
114
|
+
name: 'my-instance',
|
|
115
|
+
databases: [
|
|
116
|
+
{ name: 'mydb', migrationsPath: './migrations' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
projectId: 'my-gcp-project',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await init(config, '.spanner-migrate.config.json')
|
|
60
123
|
```
|
|
61
124
|
|
|
62
|
-
|
|
125
|
+
Writes the given configuration to a `.spanner-migrate.config.json` file.
|
|
63
126
|
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
### Creating a Migration
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { create, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
131
|
+
|
|
132
|
+
const databaseConfig: DatabaseConfig = {
|
|
133
|
+
name: 'mydb',
|
|
134
|
+
migrationsPath: './migrations',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await create(databaseConfig, 'add users table')
|
|
66
138
|
```
|
|
67
139
|
|
|
68
|
-
|
|
69
|
-
Rollback one migration
|
|
140
|
+
Creates a new migration file for the specified database.
|
|
70
141
|
|
|
71
|
-
|
|
142
|
+
### Applying Migrations
|
|
72
143
|
|
|
73
|
-
```
|
|
74
|
-
spanner-migrate
|
|
144
|
+
```typescript
|
|
145
|
+
import { up, type Config, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
146
|
+
|
|
147
|
+
// Load configuration
|
|
148
|
+
const config: Config = /* Load from file or define inline */
|
|
149
|
+
|
|
150
|
+
// Apply all migrations to all databases
|
|
151
|
+
await up(config)
|
|
152
|
+
|
|
153
|
+
// Apply all migrations to a specific database
|
|
154
|
+
const databaseConfig: DatabaseConfig = config.instance.databases[0]
|
|
155
|
+
await up(config, databaseConfig)
|
|
156
|
+
|
|
157
|
+
// Apply up to 5 migrations to a specific database
|
|
158
|
+
await up(config, databaseConfig, 5)
|
|
75
159
|
```
|
|
76
160
|
|
|
77
|
-
|
|
78
|
-
|
|
161
|
+
- Applies pending migrations.
|
|
162
|
+
- If a database is specified, only applies migrations to that database.
|
|
163
|
+
- If `max` is specified, applies at most `max` migrations.
|
|
79
164
|
|
|
80
|
-
|
|
165
|
+
### Rolling Back Migrations
|
|
81
166
|
|
|
82
|
-
```
|
|
83
|
-
spanner-migrate
|
|
167
|
+
```typescript
|
|
168
|
+
import { up, type Config, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
169
|
+
|
|
170
|
+
const config: Config = /* Load from file */
|
|
171
|
+
const databaseConfig: DatabaseConfig = config.instance.databases[0]
|
|
172
|
+
|
|
173
|
+
// Roll back the last applied migration
|
|
174
|
+
await down(config, databaseConfig)
|
|
84
175
|
```
|
|
85
|
-
Displays an overview of applied and peding migrations
|
|
86
176
|
|
|
87
|
-
|
|
88
|
-
|
|
177
|
+
- Rolls back the last applied migration for the specified database.
|
|
178
|
+
- Requires a database to be specified.
|
|
179
|
+
|
|
180
|
+
### Checking Migration Status
|
|
89
181
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
20250122080434866_add_users_table
|
|
93
|
-
20250122080444982_add_index_on_users
|
|
182
|
+
```typescript
|
|
183
|
+
import { up, type Config, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
94
184
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
185
|
+
const config: Config = /* Load from file */
|
|
186
|
+
|
|
187
|
+
// Check status for all databases
|
|
188
|
+
const migrationStatus = await status(config)
|
|
189
|
+
console.log(migrationStatus)
|
|
190
|
+
|
|
191
|
+
// Check status for a specific database
|
|
192
|
+
const databaseConfig = config.instance.databases[0]
|
|
193
|
+
const migrationStatusSingle = await status(config, [databaseConfig])
|
|
194
|
+
console.log(migrationStatusSingle)
|
|
98
195
|
```
|
|
99
196
|
|
|
100
|
-
|
|
197
|
+
- Displays applied and pending migrations for one or more databases.
|
|
198
|
+
- If a specific database is provided, only its status is shown.
|
|
199
|
+
|
|
200
|
+
## Running on Spanner Emulator
|
|
201
|
+
|
|
202
|
+
If you want to test your migrations against a Spanner Emulator, you will need to set:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
process.env.SPANNER_EMULATOR_HOST = 'localhost:<port>'
|
|
206
|
+
```
|
|
101
207
|
|
|
102
208
|
## License
|
|
103
209
|
|
|
@@ -38,7 +38,7 @@ var applyDown = async (db) => {
|
|
|
38
38
|
json: true
|
|
39
39
|
};
|
|
40
40
|
const [rows] = await db.run(req);
|
|
41
|
-
const lastMigration = rows
|
|
41
|
+
const lastMigration = rows?.[0];
|
|
42
42
|
if (!lastMigration) {
|
|
43
43
|
throw new Error("No migrations found to roll back.");
|
|
44
44
|
}
|
|
@@ -70,12 +70,11 @@ var runScript = async (db, script) => {
|
|
|
70
70
|
}
|
|
71
71
|
for (const statement of statements) {
|
|
72
72
|
console.log(`Executing statement: ${statement}`);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await db.updateSchema(sql);
|
|
73
|
+
if (isSchemaChange(statement)) {
|
|
74
|
+
await db.updateSchema(statement);
|
|
76
75
|
} else {
|
|
77
76
|
await db.runTransactionAsync(async (transaction) => {
|
|
78
|
-
await transaction.runUpdate(
|
|
77
|
+
await transaction.runUpdate(statement);
|
|
79
78
|
await transaction.commit();
|
|
80
79
|
});
|
|
81
80
|
}
|
|
@@ -101,12 +100,12 @@ var SQL_CREATE_TABLE_MIGRATIONS = `
|
|
|
101
100
|
down STRING(1024)
|
|
102
101
|
) PRIMARY KEY (id)
|
|
103
102
|
`;
|
|
104
|
-
var ensureMigrationTable = async (
|
|
105
|
-
const [rows] = await
|
|
103
|
+
var ensureMigrationTable = async (db) => {
|
|
104
|
+
const [rows] = await db.run(SQL_SELECT_TABLE_MIGRATIONS);
|
|
106
105
|
if (rows.length) return;
|
|
107
106
|
console.log("Creating migration table");
|
|
108
107
|
try {
|
|
109
|
-
await
|
|
108
|
+
await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
|
|
110
109
|
} catch (err) {
|
|
111
110
|
console.error("Failed to create migrations table");
|
|
112
111
|
throw err;
|
|
@@ -132,12 +131,12 @@ var getAppliedMigrations = async (db) => {
|
|
|
132
131
|
};
|
|
133
132
|
|
|
134
133
|
// src/files.ts
|
|
135
|
-
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
134
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
136
135
|
import { join, resolve } from "node:path";
|
|
137
136
|
var getMigrationFiles = async (path) => {
|
|
138
137
|
try {
|
|
139
138
|
const files = await readdir(path);
|
|
140
|
-
const migrationFileIds = files.filter((file) => file.endsWith(".
|
|
139
|
+
const migrationFileIds = files.filter((file) => file.endsWith(".sql")).map((file) => file.replace(/\.sql$/, ""));
|
|
141
140
|
return migrationFileIds;
|
|
142
141
|
} catch (error) {
|
|
143
142
|
throw new Error(
|
|
@@ -147,35 +146,39 @@ var getMigrationFiles = async (path) => {
|
|
|
147
146
|
};
|
|
148
147
|
var getMigration = async (path, id) => {
|
|
149
148
|
try {
|
|
150
|
-
const filePath = resolve(process.cwd(), join(path, `${id}.
|
|
149
|
+
const filePath = resolve(process.cwd(), join(path, `${id}.sql`));
|
|
151
150
|
try {
|
|
152
151
|
await access(filePath);
|
|
153
152
|
} catch (err) {
|
|
154
153
|
throw new Error(`Migration file not found: ${filePath}`);
|
|
155
154
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
155
|
+
const migrationText = await readFile(filePath, "utf8");
|
|
156
|
+
const up2 = getSql(migrationText, "up");
|
|
157
|
+
const down2 = getSql(migrationText, "down");
|
|
158
|
+
const description = getDescription(migrationText);
|
|
159
|
+
if (!up2 || !down2) {
|
|
158
160
|
throw new Error(
|
|
159
161
|
`Migration file ${filePath} does not export required scripts (up, down).`
|
|
160
162
|
);
|
|
161
163
|
}
|
|
162
|
-
return {
|
|
163
|
-
id,
|
|
164
|
-
description: id.split("_").slice(1).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "),
|
|
165
|
-
// Generate a human-readable description
|
|
166
|
-
up: migrationModule.up,
|
|
167
|
-
down: migrationModule.down
|
|
168
|
-
};
|
|
164
|
+
return { id, description, up: up2, down: down2 };
|
|
169
165
|
} catch (error) {
|
|
170
166
|
throw new Error(
|
|
171
167
|
`Failed to get migration ${id}: ${error.message}`
|
|
172
168
|
);
|
|
173
169
|
}
|
|
174
170
|
};
|
|
171
|
+
var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
172
|
+
var getSql = (text, direction) => {
|
|
173
|
+
const rx = {
|
|
174
|
+
up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
|
|
175
|
+
down: /---- DOWN ----\n([\s\S]*)$/
|
|
176
|
+
};
|
|
177
|
+
return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
|
|
178
|
+
};
|
|
175
179
|
var getNewMigrations = (applied, files) => {
|
|
176
180
|
const sortedFiles = files.sort();
|
|
177
181
|
for (let ix = 0; ix < applied.length; ix++) {
|
|
178
|
-
console.log(sortedFiles[ix], applied[ix].id);
|
|
179
182
|
if (sortedFiles[ix] !== applied[ix].id) {
|
|
180
183
|
throw new Error(
|
|
181
184
|
`Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
|
|
@@ -183,25 +186,24 @@ var getNewMigrations = (applied, files) => {
|
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
const newMigrations = sortedFiles.slice(applied.length);
|
|
186
|
-
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
187
189
|
return newMigrations;
|
|
188
190
|
};
|
|
189
191
|
var createMigration = async (path, description) => {
|
|
190
192
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
191
193
|
const compactTimestamp = timestamp.replace(/[-:.TZ]/g, "");
|
|
192
194
|
const parsedDescription = description.replace(/\s+/g, "_").toLowerCase();
|
|
193
|
-
const filename = `${compactTimestamp}_${parsedDescription}.
|
|
195
|
+
const filename = `${compactTimestamp}_${parsedDescription}.sql`;
|
|
194
196
|
const filePath = join(path, filename);
|
|
195
|
-
const template =
|
|
196
|
-
|
|
197
|
+
const template = `-- Created: ${timestamp}
|
|
198
|
+
-- Description: ${description}
|
|
199
|
+
|
|
200
|
+
---- UP ----
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
---- DOWN ----
|
|
197
205
|
|
|
198
|
-
export const up = \`
|
|
199
|
-
-- SQL for migrate up
|
|
200
|
-
\`
|
|
201
206
|
|
|
202
|
-
export const down = \`
|
|
203
|
-
-- SQL for migrate down
|
|
204
|
-
\`
|
|
205
207
|
`;
|
|
206
208
|
try {
|
|
207
209
|
await mkdir(path, { recursive: true });
|
|
@@ -234,42 +236,74 @@ var init = async (config, configPath) => {
|
|
|
234
236
|
var create = async (config, description) => {
|
|
235
237
|
await createMigration(config.migrationsPath, description);
|
|
236
238
|
};
|
|
237
|
-
var up = async (config, max
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
239
|
+
var up = async (config, database, max) => {
|
|
240
|
+
if (max && !database) {
|
|
241
|
+
throw new Error("Max number of migrations requires specifying a database");
|
|
242
|
+
}
|
|
243
|
+
const databases = database ? [database] : config.instance.databases;
|
|
244
|
+
for (const databaseConfig of databases) {
|
|
245
|
+
const path = {
|
|
246
|
+
projectId: config.projectId,
|
|
247
|
+
instanceName: config.instance.name,
|
|
248
|
+
databaseName: databaseConfig.name
|
|
249
|
+
};
|
|
250
|
+
const db = getDb(path);
|
|
251
|
+
await ensureMigrationTable(db);
|
|
252
|
+
const appliedMigrations = await getAppliedMigrations(db);
|
|
253
|
+
const migrationFiles = await getMigrationFiles(
|
|
254
|
+
databaseConfig.migrationsPath
|
|
255
|
+
);
|
|
256
|
+
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
257
|
+
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
258
|
+
console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
|
|
259
|
+
const newMigrationsToApply = max ? newMigrations.slice(0, max) : newMigrations;
|
|
260
|
+
for (const id of newMigrationsToApply) {
|
|
261
|
+
const migration = await getMigration(databaseConfig.migrationsPath, id);
|
|
262
|
+
await applyUp(db, migration);
|
|
263
|
+
}
|
|
248
264
|
}
|
|
249
265
|
};
|
|
250
|
-
var down = async (config) => {
|
|
251
|
-
const
|
|
266
|
+
var down = async (config, database) => {
|
|
267
|
+
const path = {
|
|
268
|
+
projectId: config.projectId,
|
|
269
|
+
instanceName: config.instance.name,
|
|
270
|
+
databaseName: database.name
|
|
271
|
+
};
|
|
272
|
+
const db = getDb(path);
|
|
252
273
|
await ensureMigrationTable(db);
|
|
253
274
|
await applyDown(db);
|
|
254
275
|
};
|
|
255
|
-
var status = async (config) => {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
var status = async (config, databases) => {
|
|
277
|
+
const statuses = [];
|
|
278
|
+
for (const databaseConfig of databases || config.instance.databases) {
|
|
279
|
+
const path = {
|
|
280
|
+
projectId: config.projectId,
|
|
281
|
+
instanceName: config.instance.name,
|
|
282
|
+
databaseName: databaseConfig.name
|
|
283
|
+
};
|
|
284
|
+
const db = getDb(path);
|
|
285
|
+
await ensureMigrationTable(db);
|
|
286
|
+
const appliedMigrations = await getAppliedMigrations(db);
|
|
287
|
+
const migrationFiles = await getMigrationFiles(
|
|
288
|
+
databaseConfig.migrationsPath
|
|
289
|
+
);
|
|
290
|
+
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
291
|
+
statuses.push(
|
|
292
|
+
[
|
|
293
|
+
`Migrations [${databaseConfig.name}]`,
|
|
294
|
+
"",
|
|
295
|
+
"Applied",
|
|
296
|
+
"--------------------------------------------------------------------------------",
|
|
297
|
+
`${appliedMigrations.map((m) => m.id).join("\n")}
|
|
267
298
|
`,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
299
|
+
"New",
|
|
300
|
+
"--------------------------------------------------------------------------------",
|
|
301
|
+
`${newMigrations.join("\n")}
|
|
271
302
|
`
|
|
272
|
-
|
|
303
|
+
].join("\n")
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return statuses.join("\n\n");
|
|
273
307
|
};
|
|
274
308
|
|
|
275
309
|
export {
|