2.3 KiB
2.3 KiB
Database Layer
Column Types
- Bigint (
mode: 'bigint'): All Discord snowflake IDs (userId,guildId,roleId,channelId), currency amounts (balance,xp, transactionamount, itemprice, inventoryquantity). Use0nBigInt literals, neverNumber(). - Integer: Small counts and internal IDs —
level,daily_streak,progress,reward_amountin lootdrops, auto-incrementserial('id')for items.
Composite Primary Keys
inventory(userId, itemId)— one stack per user per itemuserQuests(userId, questId)— one assignment per user per questuserTimers(userId, type, key)— one timer per user per type/key combo
Constraints
inventory.quantity > 0check constraint — never store zero-quantity rowsclasses.nameanditems.nameare unique- Cascade deletes on user FK; set null on
relatedUserId(preserves transaction history)
JSON Columns (JSONB)
quests.requirements:{ target: number }quests.rewards:{ xp?: number, balance?: number }gameSettingsfields: Typed via.$type<T>()—LevelingConfig,EconomyConfig, etc.guildSettings.featureOverrides:Record<string, boolean>(sparse)guildSettings.colorRoleIds:string[]users.settings,userTimers.metadata,items.usageData: Untyped JSONB, default{}
Enums
Defined as TypeScript enums in shared/lib/constants.ts, not as database enums. Schema columns use varchar with length constraints. Key enum types:
TimerType: COOLDOWN, EFFECT, ACCESS, EXAM_SYSTEM, TRIVIA_COOLDOWNTransactionType: TRANSFER_IN, DAILY_REWARD, etc. (8 values)ModerationCaseType: warn, timeout, kick, ban, note, pruneItemType: MATERIAL, CONSUMABLE, EQUIPMENT, QUEST
Notable Indexes
user_timers_lookup_idx: Composite on (userId, type, key) — fast timer checksuser_timers_expires_at_idx: Expiry-based cleanup queriesusers_balance_idx/users_level_xp_idx: Leaderboard queries
Client Setup
DrizzleClientis a singleton inshared/db/DrizzleClient.ts(postgres-js driver, no prefetch).withTransactionutility inbot/lib/db.tswraps Drizzle transactions and tracks count for graceful shutdown. Accepts optional existingtxfor nested calls.- No soft deletes anywhere.
moderationCasesusesactive: boolean+resolvedAt/resolvedByfor lifecycle, but rows are never deleted.