spooder 4.5.8 → 4.5.9
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 +42 -1
- package/bun.lock +51 -0
- package/package.json +1 -1
- package/src/api.ts +75 -22
package/README.md
CHANGED
|
@@ -1550,7 +1550,28 @@ Each revision should be clearly marked with a comment containing the revision nu
|
|
|
1550
1550
|
|
|
1551
1551
|
Everything following a revision header is considered part of that revision until the next revision header or the end of the file, allowing for multiple SQL statements to be included in a single revision.
|
|
1552
1552
|
|
|
1553
|
-
When calling `db_update_schema_sqlite`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date.
|
|
1553
|
+
When calling `db_update_schema_sqlite`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date.
|
|
1554
|
+
|
|
1555
|
+
It is acceptable to omit keys. This can be useful to prevent repitition when managing stored procedures, views or functions.
|
|
1556
|
+
|
|
1557
|
+
```sql
|
|
1558
|
+
-- example of repetitive declaration
|
|
1559
|
+
|
|
1560
|
+
-- [1] create view
|
|
1561
|
+
CREATE VIEW `view_test` AS SELECT * FROM `table_a` WHERE col = 'foo';
|
|
1562
|
+
|
|
1563
|
+
-- [2] change view
|
|
1564
|
+
DROP VIEW IF EXISTS `view_test`;
|
|
1565
|
+
CREATE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
|
|
1566
|
+
```
|
|
1567
|
+
Instead of unnecessarily including each full revision of a procedure, view or function in the schema file, simply store the most up-to-date one and increment the version.
|
|
1568
|
+
```sql
|
|
1569
|
+
-- [2] create view
|
|
1570
|
+
CREATE OR REPLACE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
|
|
1554
1575
|
|
|
1555
1576
|
```sql
|
|
1556
1577
|
CREATE TABLE db_schema (
|
|
@@ -1559,6 +1580,12 @@ CREATE TABLE db_schema (
|
|
|
1559
1580
|
);
|
|
1560
1581
|
```
|
|
1561
1582
|
|
|
1583
|
+
The table used for schema tracking can be changed if necessary by providing an alternative table name as the third paramater.
|
|
1584
|
+
|
|
1585
|
+
```ts
|
|
1586
|
+
await db_update_schema_sqlite(db, './schema', 'my_schema_table');
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1562
1589
|
>[!IMPORTANT]
|
|
1563
1590
|
> The entire process is transactional. If an error occurs during the application of **any** revision for **any** table, the entire process will be rolled back and the database will be left in the state it was before the update was attempted.
|
|
1564
1591
|
|
|
@@ -1575,6 +1602,20 @@ try {
|
|
|
1575
1602
|
}
|
|
1576
1603
|
```
|
|
1577
1604
|
|
|
1605
|
+
### Schema Dependencies
|
|
1606
|
+
By default, schema files are executed in the order they are provided by the operating system (generally alphabetically).
|
|
1607
|
+
|
|
1608
|
+
If you have a schema file that depends on one or more other schema files to be executed before it (for example, using foreign keys), you can specify dependencies.
|
|
1609
|
+
|
|
1610
|
+
```sql
|
|
1611
|
+
-- [deps] table_b_schema.sql, table_c_schema.sql
|
|
1612
|
+
-- [1] create table_a
|
|
1613
|
+
CREATE ...
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
>[!IMPORTANT]
|
|
1617
|
+
> Cyclic or missing dependencies will throw an error.
|
|
1618
|
+
|
|
1578
1619
|
## Legal
|
|
1579
1620
|
This software is provided as-is with no warranty or guarantee. The authors of this project are not responsible or liable for any problems caused by using this software or any part thereof. Use of this software does not entitle you to any support or assistance from the authors of this project.
|
|
1580
1621
|
|
package/bun.lock
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "spooder",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "^1.1.8",
|
|
8
|
+
},
|
|
9
|
+
"optionalDependencies": {
|
|
10
|
+
"mysql2": "^3.11.0",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"packages": {
|
|
15
|
+
"@types/bun": ["@types/bun@1.2.0", "", { "dependencies": { "bun-types": "1.2.0" } }, "sha512-5N1JqdahfpBlAv4wy6svEYcd/YfO2GNrbL95JOmFx8nkE6dbK4R0oSE5SpBA4vBRqgrOUAXF8Dpiz+gi7r80SA=="],
|
|
16
|
+
|
|
17
|
+
"@types/node": ["@types/node@22.10.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw=="],
|
|
18
|
+
|
|
19
|
+
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
|
20
|
+
|
|
21
|
+
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
|
22
|
+
|
|
23
|
+
"bun-types": ["bun-types@1.2.0", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-KEaJxyZfbV/c4eyG0vyehDpYmBGreNiQbZIqvVHJwZ4BmeuWlNZ7EAzMN2Zcd7ailmS/tGVW0BgYbGf+lGEpWw=="],
|
|
24
|
+
|
|
25
|
+
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
|
26
|
+
|
|
27
|
+
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
|
28
|
+
|
|
29
|
+
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
30
|
+
|
|
31
|
+
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
|
32
|
+
|
|
33
|
+
"long": ["long@5.2.4", "", {}, "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg=="],
|
|
34
|
+
|
|
35
|
+
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
|
|
36
|
+
|
|
37
|
+
"lru.min": ["lru.min@1.1.1", "", {}, "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q=="],
|
|
38
|
+
|
|
39
|
+
"mysql2": ["mysql2@3.12.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw=="],
|
|
40
|
+
|
|
41
|
+
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
|
42
|
+
|
|
43
|
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
44
|
+
|
|
45
|
+
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
|
46
|
+
|
|
47
|
+
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
|
48
|
+
|
|
49
|
+
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -246,6 +246,46 @@ export async function generate_hash_subs(length = 7, prefix = 'hash=', hashes?:
|
|
|
246
246
|
return hash_map;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
interface DependencyTarget {
|
|
250
|
+
file_name: string;
|
|
251
|
+
deps: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function order_schema_dep_tree<T extends DependencyTarget>(deps: T[]): T[] {
|
|
255
|
+
const visited = new Set<string>();
|
|
256
|
+
const temp = new Set<string>();
|
|
257
|
+
const result: T[] = [];
|
|
258
|
+
const map = new Map(deps.map(d => [d.file_name, d]));
|
|
259
|
+
|
|
260
|
+
function visit(node: T): void {
|
|
261
|
+
if (temp.has(node.file_name))
|
|
262
|
+
throw new Error(`Cyclic dependency {${node.file_name}}`);
|
|
263
|
+
|
|
264
|
+
if (visited.has(node.file_name))
|
|
265
|
+
return;
|
|
266
|
+
|
|
267
|
+
temp.add(node.file_name);
|
|
268
|
+
|
|
269
|
+
for (const dep of node.deps) {
|
|
270
|
+
const dep_node = map.get(dep);
|
|
271
|
+
if (!dep_node)
|
|
272
|
+
throw new Error(`Missing dependency {${dep}}`);
|
|
273
|
+
|
|
274
|
+
visit(dep_node as T);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
temp.delete(node.file_name);
|
|
278
|
+
visited.add(node.file_name);
|
|
279
|
+
result.push(node);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const dep of deps)
|
|
283
|
+
if (!visited.has(dep.file_name))
|
|
284
|
+
visit(dep);
|
|
285
|
+
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
249
289
|
type Row_DBSchema = { db_schema_table_name: string, db_schema_version: number };
|
|
250
290
|
type SchemaVersionMap = Map<string, number>;
|
|
251
291
|
|
|
@@ -268,23 +308,31 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
|
|
|
268
308
|
const schema_path = path.join(schema_file_ent.parentPath, schema_file);
|
|
269
309
|
const schema = await fs.readFile(schema_path, 'utf8');
|
|
270
310
|
|
|
311
|
+
const deps = new Array<string>();
|
|
312
|
+
|
|
271
313
|
const revisions = new Map();
|
|
272
314
|
let current_rev_id = 0;
|
|
273
315
|
let current_rev = '';
|
|
274
316
|
|
|
275
317
|
for (const line of schema.split(/\r?\n/)) {
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
318
|
+
const line_identifier = line.match(/^--\s*\[(\d+|deps)\]/);
|
|
319
|
+
if (line_identifier !== null) {
|
|
320
|
+
if (line_identifier[1] === 'deps') {
|
|
321
|
+
// Line contains schema dependencies, example: -- [deps] schema_b.sql,schema_c.sql
|
|
322
|
+
const deps_raw = line.substring(line.indexOf('deps') + 4);
|
|
323
|
+
deps.push(...deps_raw.split(',').map(e => e.trim().toLowerCase()));
|
|
324
|
+
} else {
|
|
325
|
+
// New chunk definition detected, store the current chunk and start a new one.
|
|
326
|
+
if (current_rev_id > 0) {
|
|
327
|
+
revisions.set(current_rev_id, current_rev);
|
|
328
|
+
current_rev = '';
|
|
329
|
+
}
|
|
283
330
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
331
|
+
const rev_number = parseInt(line_identifier[1]);
|
|
332
|
+
if (isNaN(rev_number) || rev_number < 1)
|
|
333
|
+
throw new Error(rev_number + ' is not a valid revision number in ' + schema_file_lower);
|
|
334
|
+
current_rev_id = rev_number;
|
|
335
|
+
}
|
|
288
336
|
} else {
|
|
289
337
|
// Append to existing revision.
|
|
290
338
|
current_rev += line + '\n';
|
|
@@ -300,35 +348,40 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
|
|
|
300
348
|
continue;
|
|
301
349
|
}
|
|
302
350
|
|
|
351
|
+
if (deps.length > 0)
|
|
352
|
+
log('[{db}] {%s} dependencies: %s', schema_file, deps.map(e => '{' + e +'}').join(', '));
|
|
353
|
+
|
|
303
354
|
const current_schema_version = schema_versions.get(schema_name) ?? 0;
|
|
304
355
|
schema_out.push({
|
|
305
356
|
revisions,
|
|
357
|
+
file_name: schema_file_lower,
|
|
306
358
|
name: schema_name,
|
|
307
359
|
current_version: current_schema_version,
|
|
360
|
+
deps,
|
|
308
361
|
chunk_keys: Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b)
|
|
309
362
|
});
|
|
310
363
|
}
|
|
311
364
|
|
|
312
|
-
return schema_out;
|
|
365
|
+
return order_schema_dep_tree(schema_out);
|
|
313
366
|
}
|
|
314
367
|
|
|
315
|
-
export async function db_update_schema_sqlite(db: Database, schema_dir: string) {
|
|
368
|
+
export async function db_update_schema_sqlite(db: Database, schema_dir: string, schema_table_name = 'db_schema') {
|
|
316
369
|
log('[{db}] updating database schema for {%s}', db.filename);
|
|
317
370
|
|
|
318
371
|
const schema_versions = new Map();
|
|
319
372
|
|
|
320
373
|
try {
|
|
321
|
-
const query = db.query('SELECT db_schema_table_name, db_schema_version FROM
|
|
374
|
+
const query = db.query('SELECT db_schema_table_name, db_schema_version FROM ' + schema_table_name);
|
|
322
375
|
for (const row of query.all() as Array<Row_DBSchema>)
|
|
323
376
|
schema_versions.set(row.db_schema_table_name, row.db_schema_version);
|
|
324
377
|
} catch (e) {
|
|
325
|
-
log('[{db}] creating {
|
|
326
|
-
db.run(
|
|
378
|
+
log('[{db}] creating {%s} table', schema_table_name);
|
|
379
|
+
db.run(`CREATE TABLE ${schema_table_name} (db_schema_table_name TEXT PRIMARY KEY, db_schema_version INTEGER)`);
|
|
327
380
|
}
|
|
328
381
|
|
|
329
382
|
db.transaction(async () => {
|
|
330
383
|
const update_schema_query = db.prepare(`
|
|
331
|
-
INSERT INTO
|
|
384
|
+
INSERT INTO ${schema_table_name} (db_schema_version, db_schema_table_name) VALUES (?1, ?2)
|
|
332
385
|
ON CONFLICT(db_schema_table_name) DO UPDATE SET db_schema_version = EXCLUDED.db_schema_version
|
|
333
386
|
`);
|
|
334
387
|
|
|
@@ -351,7 +404,7 @@ export async function db_update_schema_sqlite(db: Database, schema_dir: string)
|
|
|
351
404
|
})();
|
|
352
405
|
}
|
|
353
406
|
|
|
354
|
-
export async function db_update_schema_mysql(db: mysql_types.Connection, schema_dir: string) {
|
|
407
|
+
export async function db_update_schema_mysql(db: mysql_types.Connection, schema_dir: string, schema_table_name = 'db_schema') {
|
|
355
408
|
if (mysql === undefined)
|
|
356
409
|
throw new Error('{db_update_schema_mysql} cannot be called without optional dependency {mysql2} installed');
|
|
357
410
|
|
|
@@ -360,18 +413,18 @@ export async function db_update_schema_mysql(db: mysql_types.Connection, schema_
|
|
|
360
413
|
const schema_versions = new Map();
|
|
361
414
|
|
|
362
415
|
try {
|
|
363
|
-
const [rows] = await db.query('SELECT db_schema_table_name, db_schema_version FROM
|
|
416
|
+
const [rows] = await db.query('SELECT db_schema_table_name, db_schema_version FROM ' + schema_table_name);
|
|
364
417
|
for (const row of rows as Array<Row_DBSchema>)
|
|
365
418
|
schema_versions.set(row.db_schema_table_name, row.db_schema_version);
|
|
366
419
|
} catch (e) {
|
|
367
|
-
log('[{db}] creating {
|
|
368
|
-
await db.query(
|
|
420
|
+
log('[{db}] creating {%s} table', schema_table_name);
|
|
421
|
+
await db.query(`CREATE TABLE ${schema_table_name} (db_schema_table_name VARCHAR(255) PRIMARY KEY, db_schema_version INT)`);
|
|
369
422
|
}
|
|
370
423
|
|
|
371
424
|
await db.beginTransaction();
|
|
372
425
|
|
|
373
426
|
const update_schema_query = await db.prepare(`
|
|
374
|
-
INSERT INTO
|
|
427
|
+
INSERT INTO ${schema_table_name} (db_schema_version, db_schema_table_name) VALUES (?, ?)
|
|
375
428
|
ON DUPLICATE KEY UPDATE db_schema_version = VALUES(db_schema_version);
|
|
376
429
|
`);
|
|
377
430
|
|