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 +16 -5
- package/bun.lock +3 -3
- package/package.json +1 -1
- package/src/api.ts +4 -1
- package/src/api_db.ts +129 -67
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
|
|
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
|
-
-- [
|
|
2675
|
-
|
|
2676
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
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(
|
|
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
|
|
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
|
|
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:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|