@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/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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,6EAA6E;AAC7E,MAAM,OAAO,GAA2B,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEnF,gFAAgF;AAChF,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAW,CAAW,CAAC;IAChD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;QACpB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;gBACvB,CAAC,IAAI,CAAC,CAAC;gBACP,SAAS;YACX,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,CAAC;QACf,CAAC;QACD,CAAC,EAAE,CAAC;IACN,CAAC;IACD,OAAO,CAAC,CAAC,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAW,CAAC;QAC3B,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACf,CAAC,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;YAC5B,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACpC,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;gBAAE,CAAC,EAAE,CAAC;YAC9C,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACpC,CAAC,IAAI,CAAC,CAAC;YACP,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;gBAAE,CAAC,EAAE,CAAC;YACtE,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnB,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yEAAyE;AACzE,MAAM,iBAAiB,GAAG,CAAC,CAAS,EAAW,EAAE,CAC/C,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,sCAAsC,EAAE,EAAE,CAAC,CAAC,CAAC;AAWxF;;;;;;;GAOG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoC,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IAEpB,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,KAAK,MAAM,SAAS,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC;gBAAE,SAAS;YAC5C,IAAI,CAAC;gBACH,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;gBAC1D,MAAM,IAAI,KAAK,CACb,gDAAiD,KAAe,CAAC,OAAO,WAAW,IAAI,KAAK;oBAC1F,qFAAqF;oBACrF,gDAAgD,EAClD,EAAE,KAAK,EAAE,CACV,CAAC;YACJ,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;QAC9C,MAAM,KAAK,GAAG,EAAE;aACb,OAAO,CAAC,gGAAgG,CAAC;aACzG,GAAG,EAAwC,CAAC;QAE/C,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAuC,CAAC;YACjH,MAAM,UAAU,GACd,EAAE,CAAC,OAAO,CAAC,gEAAgE,CAAC,CAAC,GAAG,CAAC,IAAI,CAGtF,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAErB,MAAM,OAAO,GAAa,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,EAAE;iBACf,OAAO,CAAC,0EAA0E,CAAC;iBACnF,GAAG,CAAC,IAAI,CAAsF,CAAC;YAClG,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,mEAAmE;gBACnE,sEAAsE;gBACtE,0DAA0D;gBAC1D,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;oBAAE,SAAS;gBACpE,MAAM,IAAI,GACR,EAAE,CAAC,OAAO,CAAC,sDAAsD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAGlF,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBACrB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAChC,CAAC;YAED,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACvB,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/model-emit",
3
- "version": "0.4.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.77.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",