spooder 5.1.9 → 5.1.11

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
@@ -2666,16 +2666,27 @@ try {
2666
2666
  ```
2667
2667
 
2668
2668
  ### Schema Dependencies
2669
- By default, schema files are executed in the order they are provided by the operating system (generally alphabetically).
2669
+ By default, schema files are executed in the order they are provided by the operating system (generally alphabetically). Individual revisions within files are always executed in ascending order.
2670
2670
 
2671
- 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.
2671
+ If a specific revision depends on one or more other schema files to be executed before it (for example, when adding foreign keys), you can specify dependencies at the revision level.
2672
2672
 
2673
2673
  ```sql
2674
- -- [deps] table_b_schema.sql, table_c_schema.sql
2675
- -- [1] create table_a
2676
- CREATE ...
2674
+ -- [1] create table_a (no dependencies)
2675
+ CREATE TABLE table_a (
2676
+ id INTEGER PRIMARY KEY,
2677
+ name TEXT NOT NULL
2678
+ );
2679
+
2680
+ -- [2] add foreign key to table_b
2681
+ -- [deps] table_b_schema.sql
2682
+ ALTER TABLE table_a ADD COLUMN table_b_id INTEGER REFERENCES table_b(id);
2677
2683
  ```
2678
2684
 
2685
+ When a revision specifies dependencies, all revisions of the dependent schema files will be executed before that specific revision runs. This allows you to create tables independently and then add dependencies in later revisions.
2686
+
2687
+ >[!IMPORTANT]
2688
+ > Dependencies are specified per-revision, not per-file. A `-- [deps]` line applies only to the revision it appears in.
2689
+
2679
2690
  >[!IMPORTANT]
2680
2691
  > Cyclic or missing dependencies will throw an error.
2681
2692
 
package/bun.lock CHANGED
@@ -14,7 +14,7 @@
14
14
  "packages": {
15
15
  "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
16
16
 
17
- "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
17
+ "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
18
18
 
19
19
  "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
20
20
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
30
30
 
31
- "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
31
+ "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
32
32
 
33
33
  "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
34
34
 
@@ -38,7 +38,7 @@
38
38
 
39
39
  "lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
40
40
 
41
- "mysql2": ["mysql2@3.14.3", "", { "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-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ=="],
41
+ "mysql2": ["mysql2@3.14.5", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-40hDf8LPUsuuJ2hFq+UgOuPwt2IFLIRDvMv6ez9hKbXeYuZPxDDwiJW7KdknvOsQqKznaKczOT1kELgFkhDvFg=="],
42
42
 
43
43
  "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
44
44
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "5.1.9",
4
+ "version": "5.1.11",
5
5
  "module": "./src/api.ts",
6
6
  "bin": {
7
7
  "spooder": "./src/cli.ts"
package/src/api.ts CHANGED
@@ -820,7 +820,10 @@ export function http_apply_range(file: BunFile, request: Request): BunFile {
820
820
  if (start_is_nan && end_is_nan)
821
821
  return file;
822
822
 
823
- file = file.slice(start_is_nan ? file.size - end : start, end_is_nan || start_is_nan ? undefined : end);
823
+ file = file.slice(
824
+ start_is_nan ? file.size - end : start,
825
+ end_is_nan || start_is_nan ? undefined : end + 1
826
+ );
824
827
  }
825
828
  }
826
829
  return file;
package/src/api_db.ts CHANGED
@@ -17,45 +17,87 @@ export function db_serialize_set<T extends string>(set: Set<T> | null): string {
17
17
  // endregion
18
18
 
19
19
  // region schema
20
- interface DependencyTarget {
20
+ interface RevisionTarget {
21
+ schema_name: string;
21
22
  file_name: string;
23
+ revision_id: number;
24
+ sql: string;
25
+ comment: string;
22
26
  deps: string[];
23
27
  }
24
28
 
25
- function order_schema_dep_tree<T extends DependencyTarget>(deps: T[]): T[] {
29
+ function order_revisions_by_dependency(schemas: any[]): RevisionTarget[] {
30
+ // First, create a map of schema names to all their pending revisions
31
+ const schema_revisions_map = new Map<string, RevisionTarget[]>();
32
+ const all_revisions: RevisionTarget[] = [];
33
+
34
+ for (const schema of schemas) {
35
+ const schema_revisions: RevisionTarget[] = [];
36
+ for (const rev_id of schema.chunk_keys) {
37
+ const revision = schema.revisions.get(rev_id);
38
+ const revision_target: RevisionTarget = {
39
+ schema_name: schema.name,
40
+ file_name: schema.file_name,
41
+ revision_id: rev_id,
42
+ sql: revision.sql,
43
+ comment: revision.comment,
44
+ deps: revision.deps || []
45
+ };
46
+ schema_revisions.push(revision_target);
47
+ all_revisions.push(revision_target);
48
+ }
49
+ schema_revisions_map.set(schema.name, schema_revisions);
50
+ }
51
+
52
+ // Now sort revisions using topological sort
26
53
  const visited = new Set<string>();
27
54
  const temp = new Set<string>();
28
- const result: T[] = [];
29
- const map = new Map(deps.map(d => [d.file_name, d]));
30
-
31
- function visit(node: T): void {
32
- if (temp.has(node.file_name))
33
- throw new Error(`Cyclic dependency {${node.file_name}}`);
34
-
35
- if (visited.has(node.file_name))
55
+ const result: RevisionTarget[] = [];
56
+
57
+ function get_revision_key(rev: RevisionTarget): string {
58
+ return `${rev.schema_name}:${rev.revision_id}`;
59
+ }
60
+
61
+ function visit(revision: RevisionTarget): void {
62
+ const rev_key = get_revision_key(revision);
63
+
64
+ if (temp.has(rev_key))
65
+ throw new Error(`Cyclic dependency detected involving ${rev_key}`);
66
+
67
+ if (visited.has(rev_key))
36
68
  return;
37
-
38
- temp.add(node.file_name);
39
-
40
- for (const dep of node.deps) {
41
- const dep_node = map.get(dep);
42
- if (!dep_node)
43
- throw new Error(`Missing dependency {${dep}}`);
69
+
70
+ temp.add(rev_key);
71
+
72
+ // Process dependencies: if this revision depends on a schema,
73
+ // all pending revisions of that schema must execute first
74
+ for (const dep_schema_file of revision.deps) {
75
+ const dep_schema_name = path.basename(dep_schema_file, '.sql');
76
+ const dep_revisions = schema_revisions_map.get(dep_schema_name);
77
+
78
+ if (!dep_revisions)
79
+ throw new Error(`Missing dependency schema {${dep_schema_name}} required by ${rev_key}`);
44
80
 
45
- visit(dep_node as T);
81
+ // Visit all pending revisions of the dependency schema
82
+ for (const dep_revision of dep_revisions) {
83
+ visit(dep_revision);
84
+ }
85
+ }
86
+
87
+ temp.delete(rev_key);
88
+ visited.add(rev_key);
89
+ result.push(revision);
90
+ }
91
+
92
+ // Visit all revisions
93
+ for (const revision of all_revisions) {
94
+ if (!visited.has(get_revision_key(revision))) {
95
+ visit(revision);
46
96
  }
47
-
48
- temp.delete(node.file_name);
49
- visited.add(node.file_name);
50
- result.push(node);
51
97
  }
52
-
53
- for (const dep of deps)
54
- if (!visited.has(dep.file_name))
55
- visit(dep);
56
-
98
+
57
99
  return result;
58
- }
100
+ }
59
101
 
60
102
  type Row_DBSchema = { db_schema_table_name: string, db_schema_version: number };
61
103
  type SchemaVersionMap = Map<string, number>;
@@ -79,26 +121,26 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
79
121
  const schema_path = path.join(schema_file_ent.parentPath, schema_file);
80
122
  const schema = await fs.readFile(schema_path, 'utf8');
81
123
 
82
- const deps = new Array<string>();
83
-
84
124
  const revisions = new Map();
85
125
  let current_rev_id = 0;
86
126
  let current_rev = '';
87
127
  let current_rev_comment = '';
128
+ let current_rev_deps = new Array<string>();
88
129
 
89
130
  for (const line of schema.split(/\r?\n/)) {
90
131
  const line_identifier = line.match(/^--\s*\[(\d+|deps)\]/);
91
132
  if (line_identifier !== null) {
92
133
  if (line_identifier[1] === 'deps') {
93
- // Line contains schema dependencies, example: -- [deps] schema_b.sql,schema_c.sql
134
+ // Line contains schema dependencies for current revision, example: -- [deps] schema_b.sql,schema_c.sql
94
135
  const deps_raw = line.substring(line.indexOf(']') + 1);
95
- deps.push(...deps_raw.split(',').map(e => e.trim().toLowerCase()));
136
+ current_rev_deps.push(...deps_raw.split(',').map(e => e.trim().toLowerCase()));
96
137
  } else {
97
138
  // New chunk definition detected, store the current chunk and start a new one.
98
139
  if (current_rev_id > 0) {
99
- revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment });
140
+ revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment, deps: current_rev_deps });
100
141
  current_rev = '';
101
142
  current_rev_comment = '';
143
+ current_rev_deps = new Array<string>();
102
144
  }
103
145
 
104
146
  const rev_number = parseInt(line_identifier[1]);
@@ -118,28 +160,33 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
118
160
 
119
161
  // There may be something left in current_chunk once we reach end of the file.
120
162
  if (current_rev_id > 0)
121
- revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment });
163
+ revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment, deps: current_rev_deps });
122
164
 
123
165
  if (revisions.size === 0) {
124
166
  db_log(`{${schema_file}} contains no valid revisions`);
125
167
  continue;
126
168
  }
127
169
 
128
- if (deps.length > 0)
129
- db_log(`{${schema_file}} dependencies: ${log_list(deps)}`);
130
-
131
170
  const current_schema_version = schema_versions.get(schema_name) ?? 0;
171
+ const pending_revisions = Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b);
172
+
173
+ // Log any revision-level dependencies
174
+ for (const rev_id of pending_revisions) {
175
+ const revision = revisions.get(rev_id);
176
+ if (revision.deps.length > 0)
177
+ db_log(`{${schema_file}} revision [${rev_id}] dependencies: ${log_list(revision.deps)}`);
178
+ }
179
+
132
180
  schema_out.push({
133
181
  revisions,
134
182
  file_name: schema_file_lower,
135
183
  name: schema_name,
136
184
  current_version: current_schema_version,
137
- deps,
138
- chunk_keys: Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b)
185
+ chunk_keys: pending_revisions
139
186
  });
140
187
  }
141
188
 
142
- return order_schema_dep_tree(schema_out);
189
+ return schema_out;
143
190
  }
144
191
  // endregion
145
192
 
@@ -179,20 +226,28 @@ export async function db_update_schema_mysql(db: mysql_types.Connection, schema_
179
226
  `);
180
227
 
181
228
  const schemas = await db_load_schema(schema_dir, schema_versions);
182
- for (const schema of schemas) {
183
- let newest_schema_version = schema.current_version;
184
- for (const rev_id of schema.chunk_keys) {
185
- const revision = schema.revisions.get(rev_id);
186
- const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
187
- db_log(`applying revision [{${rev_id}}]${comment_text} to {${schema.name}}`);
188
-
189
- await db.query(revision.sql);
190
- newest_schema_version = rev_id;
191
- }
229
+ const revisions = order_revisions_by_dependency(schemas);
230
+
231
+ // Track the newest version for each schema
232
+ const schema_versions_tracking = new Map<string, number>();
233
+ for (const schema of schemas)
234
+ schema_versions_tracking.set(schema.name, schema.current_version);
235
+
236
+ // Execute revisions in dependency order
237
+ for (const revision of revisions) {
238
+ const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
239
+ db_log(`applying revision [{${revision.revision_id}}]${comment_text} to {${revision.schema_name}}`);
240
+
241
+ await db.query(revision.sql);
242
+ schema_versions_tracking.set(revision.schema_name, revision.revision_id);
243
+ }
192
244
 
193
- if (newest_schema_version > schema.current_version) {
194
- db_log(`updated table {${schema.name}} to revision {${newest_schema_version}}`);
195
- await update_schema_query.execute([newest_schema_version, schema.name]);
245
+ // Update schema version tracking for all modified schemas
246
+ for (const [schema_name, newest_version] of schema_versions_tracking) {
247
+ const original_version = schemas.find(s => s.name === schema_name)?.current_version || 0;
248
+ if (newest_version > original_version) {
249
+ db_log(`updated table {${schema_name}} to revision {${newest_version}}`);
250
+ await update_schema_query.execute([newest_version, schema_name]);
196
251
  }
197
252
  }
198
253
 
@@ -451,20 +506,27 @@ export async function db_update_schema_sqlite(db: Database, schema_dir: string,
451
506
  `);
452
507
 
453
508
  const schemas = await db_load_schema(schema_dir, schema_versions);
454
-
455
- for (const schema of schemas) {
456
- let newest_schema_version = schema.current_version;
457
- for (const rev_id of schema.chunk_keys) {
458
- const revision = schema.revisions.get(rev_id);
459
- const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
460
- db_log(`applying revision [{${rev_id}}]${comment_text} to {${schema.name}}`);
461
- db.transaction(() => db.run(revision.sql))();
462
- newest_schema_version = rev_id;
463
- }
464
-
465
- if (newest_schema_version > schema.current_version) {
466
- db_log(`updated table {${schema.name}} to revision {${newest_schema_version}}`);
467
- update_schema_query.run(newest_schema_version, schema.name);
509
+ const revisions = order_revisions_by_dependency(schemas);
510
+
511
+ // Track the newest version for each schema
512
+ const schema_versions_tracking = new Map<string, number>();
513
+ for (const schema of schemas)
514
+ schema_versions_tracking.set(schema.name, schema.current_version);
515
+
516
+ // Execute revisions in dependency order
517
+ for (const revision of revisions) {
518
+ const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
519
+ db_log(`applying revision [{${revision.revision_id}}]${comment_text} to {${revision.schema_name}}`);
520
+ db.transaction(() => db.run(revision.sql))();
521
+ schema_versions_tracking.set(revision.schema_name, revision.revision_id);
522
+ }
523
+
524
+ // Update schema version tracking for all modified schemas
525
+ for (const [schema_name, newest_version] of schema_versions_tracking) {
526
+ const original_version = schemas.find(s => s.name === schema_name)?.current_version || 0;
527
+ if (newest_version > original_version) {
528
+ db_log(`updated table {${schema_name}} to revision {${newest_version}}`);
529
+ update_schema_query.run(newest_version, schema_name);
468
530
  }
469
531
  }
470
532