Files
Module-uo/server/db/purge.sql
wtclaude 2fa4d87a40
All checks were successful
PR Checks / client-build (pull_request) Successful in 15s
PR Checks / server-tests (pull_request) Successful in 19s
PR Checks / frozen-manifest (pull_request) Successful in 38s
feat(shard): ingest guild rosters and departures (protocol 4)
Protocol 2 gave the guild board a member *count* and nothing else, so the Guilds
page could say a guild had 155 members but never who they were, and
findGuildForActor deliberately answered only for leaders because membership for
rank-and-file was not in the feed at all. Protocol 4 puts it there.

`shard_guild_members` holds one row per member per guild, keyed on
(guild_id, serial). `guild.roster` replaces a guild's rows; `guild.leave` removes
one. A guild.remove now clears the membership too, so a disbanded guild does not
leave orphaned rows behind.

The chunking needs explaining. A roster over the shard's per-frame cap arrives as
several frames carrying seq/more/total. The sidecar reassembles them for its own
GET /guilds board, but the live WebSocket feed and the /history backfill both
carry the individual frames — so this ingest sees them unreassembled.

It copes without buffering, because a table expresses what the sidecar's single
JSON column could not: the frame carrying seq 0 clears the guild first, and every
frame then upserts its own rows. Upsert rather than insert because the /history
backfill replays stored frames on every reconnect, and a redelivery has to be a
no-op rather than a duplicate-key error. The cost is a sub-second window during a
multi-frame update where the table holds part of a roster; buffering to close it
would duplicate the sidecar's reassembly for a projection that is already only as
fresh as a 60s sweep.

On visibility: both kinds are mapped to the existing `guilds` feature. Without
that mapping rule 2 fails an unmapped kind closed to admin-only, which would have
quietly kept rosters off the public page forever. Mapping them is safe because a
roster is the first frame carrying locked fields inside an ARRAY of actors rather
than one nested actor, and the projection walker already recurses into arrays and
matches acct/webId by suffix — so a member's account name is stripped below admin
by exactly the rule that already strips guild.leader.acct. There is a test for
that specifically, because the difference is a public page listing character names
versus one publishing 150 account names.

`acct`/`web_id` are still stored, since that is what lets a linked member be
matched to a site user; they are just never projected below admin.

guild.leave is appended to the event log, as the departure counterpart to
guild.join and for the same reason — it is what a "so-and-so left" feed reads.
guild.roster stays out: it is board state like guild.update, and it is the one fat
frame on the wire, so logging it would put a full membership snapshot into
shard_events on every membership change.

The PUBLIC_KINDS guard test caught the addition, which is what it is for; its
expected set now carries a v4 group alongside the v3 one.

Refs: docs/website/TEAMS.md Part 12 Phase 1

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 12:57:26 -05:00

58 lines
2.9 KiB
SQL

-- ── module-uo's teardown ──────────────────────────────────────────────────
--
-- Destructive, and run ONLY by an explicit admin purge (MODULE_API.md §2.6).
-- Nothing on the boot path ever executes this file — uninstalling a module
-- leaves its data alone, and removing the data is a separate decision an
-- operator has to make on purpose.
--
-- It exists because `schema.sql` does. A module that can create tables and
-- cannot drop them leaves an operator with orphaned data and no supported way
-- to remove it, which is why core refuses to load a module that declares one
-- without the other.
--
-- **The order is the reverse of creation, and that is load-bearing**: two of
-- these tables carry a foreign key into core's `users`, and several reference
-- each other. Dropping a parent before its children fails on the constraint,
-- and a purge that fails halfway is worse than one that does not run — it
-- leaves exactly the orphaned data this file exists to remove. `IF EXISTS` on
-- every line so a partially-installed module still tears down cleanly.
--
-- What is NOT here, deliberately: rows this module wrote into core's tables.
-- `notification_subs` rows for `shard.*` streams and `announce_job_legs` rows
-- with leg `towncrier` belong to core's tables, and a module does not delete
-- from those — core prunes them when it drops the registrations, which it can
-- do because it knows which registrant owned what. The two `settings` rows
-- schema.sql seeds (`game_account_signup`, `uo_link_protocol_3_migrated`) are
-- the same case with an extra reason: the second is a one-shot MIGRATION
-- marker, and deleting it would re-arm a protocol bump against tables this
-- file has just dropped.
DROP TABLE IF EXISTS `shard_atlas_pending`;
DROP TABLE IF EXISTS `shard_atlas_meta`;
DROP TABLE IF EXISTS `shard_cliloc_meta`;
DROP TABLE IF EXISTS `shard_clilocs`;
DROP TABLE IF EXISTS `shard_champion_spawns`;
DROP TABLE IF EXISTS `shard_landmarks`;
DROP TABLE IF EXISTS `shard_regions`;
DROP TABLE IF EXISTS `shard_spawn_point_types`;
DROP TABLE IF EXISTS `shard_spawn_points`;
DROP TABLE IF EXISTS `shard_spawn_creatures`;
DROP TABLE IF EXISTS `shard_feature_visibility`;
DROP TABLE IF EXISTS `shard_vendor_items`;
DROP TABLE IF EXISTS `shard_vendors`;
DROP TABLE IF EXISTS `shard_points_boards`;
DROP TABLE IF EXISTS `shard_ruleset`;
DROP TABLE IF EXISTS `shard_presence`;
DROP TABLE IF EXISTS `shard_governor_terms`;
DROP TABLE IF EXISTS `shard_governors`;
DROP TABLE IF EXISTS `shard_guild_members`;
DROP TABLE IF EXISTS `shard_guilds`;
DROP TABLE IF EXISTS `shard_pages`;
DROP TABLE IF EXISTS `shard_champs`;
DROP TABLE IF EXISTS `shard_account_links`;
DROP TABLE IF EXISTS `shard_houses`;
DROP TABLE IF EXISTS `shard_economy`;
DROP TABLE IF EXISTS `shard_online`;
DROP TABLE IF EXISTS `shard_events`;
DROP TABLE IF EXISTS `uo_link_config`;