|
|
|
|
@@ -237,6 +237,40 @@ immediately after it, statement by statement, split the same way. It is subject
|
|
|
|
|
core's file already follows: `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE … ADD COLUMN IF NOT EXISTS`,
|
|
|
|
|
no `--` inside a string literal, no `DROP`.
|
|
|
|
|
|
|
|
|
|
"Split the same way" is shared code, not a shared description: `utils/sqlStatements.js` holds the
|
|
|
|
|
splitter and both callers use it. It is its own file rather than an export of `utils/db.js` because
|
|
|
|
|
the loader validates fragments at require time and must not pull the mariadb pool into `app.js`'s
|
|
|
|
|
require chain to do it.
|
|
|
|
|
|
|
|
|
|
**The rules above are enforced at LOAD time, not at replay time** (PR 3). Everything §2.6 states
|
|
|
|
|
about the SQL is knowable by reading the file, so a fragment that breaks a rule costs the module its
|
|
|
|
|
mount entirely (§4.4's left-hand column) rather than mounting and then 503ing with tables half
|
|
|
|
|
created. What is left for the replay is the class of failure only the database can report — an
|
|
|
|
|
unknown column type, a bad foreign key — and those are post-mount and answer 503.
|
|
|
|
|
|
|
|
|
|
**The check is a leading-verb allowlist: `CREATE`, `ALTER`, `INSERT`, `UPDATE`.** Those are the four
|
|
|
|
|
core's own `schema.sql` uses. It is an allowlist rather than the `DROP` denylist this section words
|
|
|
|
|
it as because a fragment is **replayed on every boot**: `TRUNCATE` and `DELETE` would empty a table
|
|
|
|
|
at every restart, `RENAME` would fail at the second one, and `GRANT`/`SET`/`USE` are core's business.
|
|
|
|
|
A denylist only ever bans what somebody thought of. It is a leading-verb check and claims no more:
|
|
|
|
|
`ALTER TABLE x DROP COLUMN y` passes it, and catching that needs a SQL parser — a large dependency
|
|
|
|
|
for a rule whose job is stopping the obvious foot-gun early. A `CREATE TABLE` missing `IF NOT EXISTS`
|
|
|
|
|
is rejected on the same grounds: it succeeds exactly once and fails every boot after, presenting to
|
|
|
|
|
an operator as a module that broke on restart.
|
|
|
|
|
|
|
|
|
|
**The replay is outside `ensureSchema()`'s retry loop.** Core's schema is retried ten times while the
|
|
|
|
|
database comes up; a fragment that throws is one module's failure, not a signal the database is not
|
|
|
|
|
ready, and retrying core's whole schema over one module's bad SQL would turn a 503'd module into a
|
|
|
|
|
two-minute boot. Partial application is accepted rather than compensated for — MariaDB self-commits
|
|
|
|
|
each DDL statement, so no transaction could roll back the tables created before the failing one, and
|
|
|
|
|
the idempotence rule is what makes re-running a corrected fragment safe.
|
|
|
|
|
|
|
|
|
|
**One caller replays nothing, deliberately.** `db/seed.js` (`npm run seed`) calls `ensureSchema()`
|
|
|
|
|
standalone without requiring `app.js`, so no scan has happened and `fragments()`'s §7.6 throw would
|
|
|
|
|
break seeding outright. The replay asks `isLoaded()` and logs the skip. That is the only sanctioned
|
|
|
|
|
use of that predicate: everywhere else, reading the module list before `load()` still throws, because
|
|
|
|
|
a booting server quietly getting no module tables is precisely what §7.6 exists to prevent.
|
|
|
|
|
|
|
|
|
|
**Table names are namespaced and collision-checked.** New tables must be prefixed `<id>_`. The
|
|
|
|
|
loader extracts every `CREATE TABLE IF NOT EXISTS <name>` from the fragment and rejects the module
|
|
|
|
|
if a name collides with a core table or with another module's — a wrong `DROP`-free fragment can
|
|
|
|
|
|