@substrat-run/model-emit 0.4.0 → 0.5.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/LICENSE +202 -661
- package/dist/emit-sql.d.ts +2 -1
- package/dist/emit-sql.d.ts.map +1 -1
- package/dist/emit-sql.js +21 -5
- package/dist/emit-sql.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts +24 -27
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +45 -165
- package/dist/journal.js.map +1 -1
- package/dist/replay.d.ts +26 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +156 -0
- package/dist/replay.js.map +1 -0
- package/package.json +6 -2
package/dist/replay.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The journal, read by REPLAYING it.
|
|
3
|
+
*
|
|
4
|
+
* Every reader in this package used to parse SQL with regular expressions, and
|
|
5
|
+
* every one of them was wrong in a way nobody could have predicted from reading
|
|
6
|
+
* it. #807 reported two defects; probing the same cause found five more in ten
|
|
7
|
+
* minutes — a `CREATE TABLE` on one line, a `) STRICT;` suffix, a wrapped
|
|
8
|
+
* `PRIMARY KEY (` list, a quoted `"order"` identifier, the word UNIQUE inside a
|
|
9
|
+
* comment. That is not a list of bugs, it is a shape: a regex over SQL text has
|
|
10
|
+
* no bottom, and every fix is a patch against the next spelling.
|
|
11
|
+
*
|
|
12
|
+
* So this does not read SQL. It runs it, into a throwaway in-memory database,
|
|
13
|
+
* and asks SQLite what the schema is. The answer is the one the production
|
|
14
|
+
* database would give, because it comes from the same engine — which is the
|
|
15
|
+
* whole claim `emitTables` makes and the reason a reader exists at all.
|
|
16
|
+
*
|
|
17
|
+
* **Only schema statements are replayed.** A journal's `INSERT`s do not change
|
|
18
|
+
* its schema, and skipping them is what lets a vertical's journal be read on its
|
|
19
|
+
* own: two of this repo's verticals hand data to an engine with
|
|
20
|
+
* `INSERT … SELECT` into a table that lives in the ENGINE's journal (the
|
|
21
|
+
* decision-28 extraction handoff), which no amount of replaying the vertical
|
|
22
|
+
* alone can satisfy. Foreign keys stay off — SQLite's default — so a
|
|
23
|
+
* `REFERENCES` pointing into another module's journal is created, not refused.
|
|
24
|
+
*
|
|
25
|
+
* Node-only, deliberately. This package is a devDependency in all of its
|
|
26
|
+
* dependents and no `src/` file imports it, so nothing here reaches a worker
|
|
27
|
+
* bundle or a scope; the builder runs its gates as shell commands in a
|
|
28
|
+
* container. `node:sqlite` is a builtin, so this costs no dependency.
|
|
29
|
+
*/
|
|
30
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
31
|
+
/** The quote characters SQLite accepts around an identifier or a literal. */
|
|
32
|
+
const CLOSING = { "'": "'", '"': '"', '`': '`', '[': ']' };
|
|
33
|
+
/** Index after the quoted run starting at `i`; a doubled quote is an escape. */
|
|
34
|
+
function endOfQuoted(s, i) {
|
|
35
|
+
const close = CLOSING[s[i]];
|
|
36
|
+
let j = i + 1;
|
|
37
|
+
while (j < s.length) {
|
|
38
|
+
if (s[j] === close) {
|
|
39
|
+
if (s[j + 1] === close) {
|
|
40
|
+
j += 2;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
return j + 1;
|
|
44
|
+
}
|
|
45
|
+
j++;
|
|
46
|
+
}
|
|
47
|
+
return s.length;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The journal's statements, split on the semicolons that actually end one.
|
|
51
|
+
*
|
|
52
|
+
* Quote- and comment-aware, because a `;` inside a string literal or a `--`
|
|
53
|
+
* comment ends nothing. This is the only text scanning left in the package, and
|
|
54
|
+
* it exists because the statements have to be filtered before they are run —
|
|
55
|
+
* `db.exec` would happily run the `INSERT`s too.
|
|
56
|
+
*/
|
|
57
|
+
export function statements(sql) {
|
|
58
|
+
const out = [];
|
|
59
|
+
let start = 0;
|
|
60
|
+
for (let i = 0; i < sql.length; i++) {
|
|
61
|
+
const c = sql[i];
|
|
62
|
+
if (CLOSING[c]) {
|
|
63
|
+
i = endOfQuoted(sql, i) - 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
67
|
+
while (i < sql.length && sql[i] !== '\n')
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
72
|
+
i += 2;
|
|
73
|
+
while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/'))
|
|
74
|
+
i++;
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (c === ';') {
|
|
79
|
+
const s = sql.slice(start, i).trim();
|
|
80
|
+
if (s)
|
|
81
|
+
out.push(s);
|
|
82
|
+
start = i + 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const last = sql.slice(start).trim();
|
|
86
|
+
if (last)
|
|
87
|
+
out.push(last);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** Statements that change the schema — the only ones worth replaying. */
|
|
91
|
+
const isSchemaStatement = (s) => /^(CREATE|ALTER|DROP)\b/i.test(s.replace(/^(?:\s|--[^\n]*\n|\/\*[\s\S]*?\*\/)*/, ''));
|
|
92
|
+
/**
|
|
93
|
+
* One replay per journal string.
|
|
94
|
+
*
|
|
95
|
+
* `planMigration` asks for columns, keys and uniques off the same journal, and
|
|
96
|
+
* building three identical databases to answer three questions about one schema
|
|
97
|
+
* would be silly. Keyed on the SQL itself, which is what makes it safe: the
|
|
98
|
+
* readers are pure functions of their input, and this does not change that.
|
|
99
|
+
*/
|
|
100
|
+
const cache = new Map();
|
|
101
|
+
/**
|
|
102
|
+
* The schema a journal leaves behind, as SQLite sees it.
|
|
103
|
+
*
|
|
104
|
+
* Throws if a schema statement does not apply — which is a feature, not a
|
|
105
|
+
* regression. A journal that cannot be replayed is a journal that will not apply
|
|
106
|
+
* to a real scope either, and the old readers answered anyway.
|
|
107
|
+
*/
|
|
108
|
+
export function readSchema(sql) {
|
|
109
|
+
const hit = cache.get(sql);
|
|
110
|
+
if (hit)
|
|
111
|
+
return hit;
|
|
112
|
+
const db = new DatabaseSync(':memory:');
|
|
113
|
+
try {
|
|
114
|
+
for (const statement of statements(sql)) {
|
|
115
|
+
if (!isSchemaStatement(statement))
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
db.exec(statement);
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
const head = statement.replace(/\s+/g, ' ').slice(0, 120);
|
|
122
|
+
throw new Error(`journal: a schema statement does not apply — ${cause.message}\n in: ${head}…\n` +
|
|
123
|
+
'The journal is replayed into SQLite to read its schema, so a statement that cannot ' +
|
|
124
|
+
'run here would not run against a scope either.', { cause });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const schema = new Map();
|
|
128
|
+
const names = db
|
|
129
|
+
.prepare(`SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
|
|
130
|
+
.all();
|
|
131
|
+
for (const { name } of names) {
|
|
132
|
+
const info = db.prepare(`SELECT name FROM pragma_table_info(?)`).all(name);
|
|
133
|
+
const primaryKey = db.prepare(`SELECT name FROM pragma_table_info(?) WHERE pk > 0 ORDER BY pk`).all(name).map((r) => r.name);
|
|
134
|
+
const uniques = [];
|
|
135
|
+
const indexes = db
|
|
136
|
+
.prepare(`SELECT name, "unique" AS uniq, partial, origin FROM pragma_index_list(?)`)
|
|
137
|
+
.all(name);
|
|
138
|
+
for (const index of indexes) {
|
|
139
|
+
// `pk` is the primary key wearing an index; it has its own reader.
|
|
140
|
+
// A PARTIAL index constrains a subset of the rows, so reading it as a
|
|
141
|
+
// key would claim a guarantee the database does not make.
|
|
142
|
+
if (!index.uniq || index.partial || index.origin === 'pk')
|
|
143
|
+
continue;
|
|
144
|
+
const cols = db.prepare(`SELECT name FROM pragma_index_info(?) ORDER BY seqno`).all(index.name).map((r) => r.name);
|
|
145
|
+
uniques.push(cols.join(', '));
|
|
146
|
+
}
|
|
147
|
+
schema.set(name, { columns: info.map((c) => c.name), primaryKey, uniques });
|
|
148
|
+
}
|
|
149
|
+
cache.set(sql, schema);
|
|
150
|
+
return schema;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
db.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@substrat-run/model-emit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Build-time tooling over a Substrat model — DDL emitted from the entity registry, and the journal reader that holds it honest",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@substrat-run/contracts": "0.
|
|
25
|
+
"@substrat-run/contracts": "0.79.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
28
29
|
"typescript": "^7.0.0",
|
|
29
30
|
"vitest": "^3.2.7",
|
|
30
31
|
"zod": "^4.4.3"
|
|
@@ -35,6 +36,9 @@
|
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"zod": "^4.4.0"
|
|
37
38
|
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.5"
|
|
41
|
+
},
|
|
38
42
|
"scripts": {
|
|
39
43
|
"build": "tsc -p tsconfig.json",
|
|
40
44
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|