-- ── The schema fragment ─────────────────────────────────────────────────── -- -- Core replays this file on EVERY boot, statement by statement, immediately -- after its own schema.sql and before it seeds defaults (MODULE_API.md §2.6). -- -- **There is no migration runner anywhere in this project, and that is a -- decision rather than an omission.** Core's own schema is one idempotent file -- replayed the same way. So a module's schema is not a sequence of changes to -- apply once — it is a statement of what the tables should look like, written so -- that running it against a database that already matches does nothing. -- -- Which means: every CREATE TABLE carries IF NOT EXISTS and every ALTER carries -- IF NOT EXISTS. A statement that succeeds once and fails afterwards presents as -- a module that worked until the first restart. -- -- **And it means CHANGING a table is an ALTER, never an edit to its CREATE.** -- `CREATE TABLE IF NOT EXISTS` does nothing at all when the table is already -- there, so an edited column definition takes effect on a fresh install and on no -- existing one — the worst possible split, because your development database is -- usually the fresh one. Add the column with -- `ALTER TABLE … ADD COLUMN IF NOT EXISTS`, below the CREATE, and leave the -- CREATE describing what a new install gets. -- -- ── What core checks, and when ──────────────────────────────────────────── -- -- Core validates this file at LOAD time, before your module mounts anything — -- so a rule broken here costs you the mount entirely rather than leaving you -- with half-created tables and routes that 503. What is left for replay time is -- the class only the database can answer: an unknown column type, a bad foreign -- key. Those are post-mount and do answer 503. -- -- • **Leading verbs are an allowlist: CREATE, ALTER, INSERT, UPDATE.** Not a -- DROP denylist. This file replays every boot, so a TRUNCATE or a DELETE -- would empty a table on every restart. -- • **Every table you create must be prefixed with your module id** — -- `examplegame_` here. Nothing else in the database is yours to create. -- • **No table core declares, and none another module has claimed.** -- -- A foreign key INTO a core table is allowed, and works because core's schema is -- already in place when this runs. The reverse is not, and could not be: it -- would make core's schema depend on your module being installed. -- -- Teardown is `purge.sql`, which no boot ever runs. See it. -- ── World status ────────────────────────────────────────────────────────── -- One row, id 1, holding the last thing the game server said about itself. -- -- A singleton row rather than a settings key because it is *observed state* and -- not configuration: it is written by whatever ingests from your sidecar, and an -- operator never edits it. In a real module the writer is the sidecar ingest; -- here `boot.js` writes it once so the page has something to render. -- `updated_at` carries no `ON UPDATE CURRENT_TIMESTAMP`, deliberately. That -- clause fires only when an UPDATE actually CHANGES a value, so a writer sending -- the same numbers back — which is what a quiet game looks like — leaves the -- timestamp frozen at the first write, and the row then goes stale while nothing -- is wrong. The writer sets the column explicitly instead; see -- `model/worldStatus/worldStatus.db.js`. CREATE TABLE IF NOT EXISTS examplegame_world_status ( id TINYINT UNSIGNED NOT NULL PRIMARY KEY, online TINYINT(1) NOT NULL DEFAULT 0, players INT UNSIGNED NOT NULL DEFAULT 0, world_name VARCHAR(120) NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Seed the singleton. `INSERT IGNORE` rather than a plain INSERT: this runs -- again on every boot, and the second run must be a no-op rather than a -- duplicate-key error that fails the whole replay. INSERT IGNORE INTO examplegame_world_status (id, online, players) VALUES (1, 0, 0);