spooder 4.5.8 → 4.5.10

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 (4) hide show
  1. package/README.md +42 -1
  2. package/bun.lock +49 -0
  3. package/package.json +1 -1
  4. 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. Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
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,49 @@
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.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="],
16
+
17
+ "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
18
+
19
+ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
20
+
21
+ "bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="],
22
+
23
+ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
24
+
25
+ "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
26
+
27
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
28
+
29
+ "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
30
+
31
+ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
32
+
33
+ "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
34
+
35
+ "lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
36
+
37
+ "mysql2": ["mysql2@3.14.1", "", { "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-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w=="],
38
+
39
+ "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
40
+
41
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
42
+
43
+ "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
44
+
45
+ "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
46
+
47
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.5.8",
4
+ "version": "4.5.10",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
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.reverse();
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 rev_start = line.match(/^--\s*\[(\d+)\]/);
277
- if (rev_start !== null) {
278
- // New chunk definition detected, store the current chunk and start a new one.
279
- if (current_rev_id > 0) {
280
- revisions.set(current_rev_id, current_rev);
281
- current_rev = '';
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
- const rev_number = parseInt(rev_start[1]);
285
- if (isNaN(rev_number) || rev_number < 1)
286
- throw new Error(rev_number + ' is not a valid revision number in ' + schema_file_lower);
287
- current_rev_id = rev_number;
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 db_schema');
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 {db_schema} table');
326
- db.run('CREATE TABLE db_schema (db_schema_table_name TEXT PRIMARY KEY, db_schema_version INTEGER)');
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 db_schema (db_schema_version, db_schema_table_name) VALUES (?1, ?2)
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 db_schema');
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 {db_schema} table');
368
- await db.query('CREATE TABLE db_schema (db_schema_table_name VARCHAR(255) PRIMARY KEY, db_schema_version INT)');
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 db_schema (db_schema_version, db_schema_table_name) VALUES (?, ?)
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