285 lines
7.5 KiB
Markdown
285 lines
7.5 KiB
Markdown
---
|
|
name: buzz-database
|
|
description: Drizzle ORM workflows for the Buzz PostgreSQL database
|
|
metadata:
|
|
author: dmgnr
|
|
version: 1.0.0
|
|
---
|
|
|
|
# Buzz — Database Workflows (Drizzle ORM)
|
|
|
|
This skill covers working with the PostgreSQL database via Drizzle ORM.
|
|
|
|
## Schema Overview
|
|
|
|
All tables are defined in `lib/db/schema.ts`. The database uses **three named schemas** + the public schema:
|
|
|
|
### Public schema
|
|
|
|
| Table | Purpose |
|
|
| -------------------------------------------- | --------------------------------------------------------- |
|
|
| `cdn` | File storage (bytea), used for images, cards, slips |
|
|
| `auditLog` | Admin action audit trail |
|
|
| `characters` | Genshin characters (linked to versions, CDN images) |
|
|
| `versions` | Game version tracking (self-referencing FK for `from`) |
|
|
| `settings` | Global app settings (singleton row, PK is boolean `true`) |
|
|
| `guides` | Character build guide links |
|
|
| `user`, `session`, `account`, `verification` | Better-auth tables |
|
|
|
|
### `artifact` schema
|
|
|
|
| Table | Purpose |
|
|
| ------------- | ------------------------------------------ |
|
|
| `submissions` | Artifact review queue submissions |
|
|
| `cards` | Generated character cards for submissions |
|
|
| `settings` | Artifact-specific settings (locked, limit) |
|
|
|
|
### `endgame` schema
|
|
|
|
| Table | Purpose |
|
|
| ------------- | ----------------------------------- |
|
|
| `submissions` | Rubgram (endgame) queue submissions |
|
|
| `sarchive` | Archived completed submissions |
|
|
| `expired` | Expired unpaid submissions |
|
|
| `slips` | SlipOK payment slip images and data |
|
|
| `settings` | Rubgram-specific settings |
|
|
| `discord` | Discord user mapping |
|
|
| `types` | Service types with pricing |
|
|
|
|
### `tierlist` schema
|
|
|
|
| Table | Purpose |
|
|
| ---------- | ----------------------------------------------------- |
|
|
| `types` | Tierlist categories (Spiral Abyss, Stygian Onslaught) |
|
|
| `tiers` | Tier definitions (S, A, B, etc.) |
|
|
| `columns` | Column layout definitions |
|
|
| `badges` | Badge icons for tiers |
|
|
| `versions` | Tierlist versions per type |
|
|
| `states` | Character placements per version (JSONB badges) |
|
|
|
|
### Key schema patterns
|
|
|
|
**UUIDv7 primary keys:**
|
|
|
|
```ts
|
|
import { uuidv7 } from "uuidv7";
|
|
|
|
id: uuid("id").primaryKey().$defaultFn(uuidv7),
|
|
```
|
|
|
|
**Named schemas with `pgSchema`:**
|
|
|
|
```ts
|
|
import { pgSchema } from "drizzle-orm/pg-core";
|
|
|
|
const artifact = pgSchema("artifact");
|
|
export const submissions = artifact.table("submissions", { ... });
|
|
```
|
|
|
|
**Custom `bytea` column** (for binary data like images):
|
|
|
|
```ts
|
|
import { bytea } from "$/db/custom";
|
|
// used in: cdn, cards, slips tables
|
|
```
|
|
|
|
**Generated columns:**
|
|
|
|
```ts
|
|
paid: boolean().generatedAlwaysAs(
|
|
sql`(price <= (0)::numeric) OR (slip IS NOT NULL)`
|
|
),
|
|
```
|
|
|
|
**Serial sequences** for queue numbering per schema:
|
|
|
|
```ts
|
|
queue: integer().notNull().default(sql`(nextval('artifact.submissions_queue_seq'::regclass))`),
|
|
```
|
|
|
|
**Enums:**
|
|
|
|
```ts
|
|
export const characterElement = pgEnum("character_element", [
|
|
"anemo",
|
|
"geo",
|
|
"dendro",
|
|
"hydro",
|
|
"pyro",
|
|
"cryo",
|
|
"electro",
|
|
]);
|
|
```
|
|
|
|
**Foreign keys with cascade:**
|
|
|
|
```ts
|
|
char: uuid("char").notNull().references(() => characters.id, {
|
|
onDelete: "cascade",
|
|
onUpdate: "cascade",
|
|
}),
|
|
```
|
|
|
|
## Migration Workflow
|
|
|
|
### Important: database boundary
|
|
|
|
Schema changes must be pushed to the database **before** the new code deploys. Otherwise the running app will crash on startup when it queries tables or columns that don't exist yet.
|
|
|
|
| Environment | Command | Timing |
|
|
| -------------- | ---------------- | ------------------------------------------------------------------------------------- |
|
|
| **Dev** | `bun ds dr push` | Run inside app container (via `bun ds`) against dev DB |
|
|
| **Production** | `bun dr push` | Run locally with production env vars against prod DB. **Must run before `git push`.** |
|
|
|
|
### Full migration workflow
|
|
|
|
1. **Edit schema** in `lib/db/schema.ts`
|
|
2. **Generate migration files** (optional, for tracking):
|
|
```bash
|
|
bun dr generate
|
|
```
|
|
3. **Push to dev** to verify:
|
|
```bash
|
|
bun ds dr push
|
|
```
|
|
4. **If using migrate instead of push** for production:
|
|
```bash
|
|
bun dr migrate
|
|
```
|
|
5. **Push to production DB** (with production credentials):
|
|
```bash
|
|
bun dr push
|
|
```
|
|
6. **Deploy code** via `git push` (Gitea Actions handles the rest)
|
|
|
|
When using `bun dr push`, Drizzle automatically detects differences between the schema and the database, and applies the necessary ALTER statements. For production, always verify the SQL Drizzle generates first.
|
|
|
|
### Drizzle Studio
|
|
|
|
Accessible at `http://localhost:4983` when dev containers are running. The `drizzle` service in `dev/compose.yml` provides this:
|
|
|
|
```bash
|
|
bun ds bun dr studio --host $(hostname)
|
|
```
|
|
|
|
## Common Schema Operations
|
|
|
|
### Adding a column
|
|
|
|
```ts
|
|
// In the table definition, add:
|
|
newColumn: varchar("new_column", { length: 255 }).default(""),
|
|
```
|
|
|
|
Generate + push.
|
|
|
|
### Adding a new table
|
|
|
|
```ts
|
|
export const myTable = pgSchema("myschema").table("my_table", {
|
|
id: uuid("id").primaryKey().$defaultFn(uuidv7),
|
|
name: varchar("name", { length: 255 }).notNull(),
|
|
// ...
|
|
});
|
|
```
|
|
|
|
Export and use in queries. Generate + push.
|
|
|
|
### Adding a new enum
|
|
|
|
```ts
|
|
export const myEnum = pgEnum("my_enum", ["value1", "value2"]);
|
|
|
|
export const myTable = pgSchema("myschema").table("my_table", {
|
|
status: myEnum("status").default("value1"),
|
|
});
|
|
```
|
|
|
|
### Adding foreign key
|
|
|
|
```ts
|
|
refId: uuid("ref_id")
|
|
.notNull()
|
|
.references(() => otherTable.id, {
|
|
onDelete: "cascade",
|
|
onUpdate: "cascade",
|
|
}),
|
|
```
|
|
|
|
## Query patterns
|
|
|
|
### Basic select
|
|
|
|
```ts
|
|
import { db } from "$/db";
|
|
import { submissions } from "$/db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
|
|
const items = await db
|
|
.select()
|
|
.from(submissions)
|
|
.where(eq(submissions.checked, false));
|
|
```
|
|
|
|
### Select with join
|
|
|
|
```ts
|
|
const result = await db
|
|
.select({
|
|
name: characters.name,
|
|
queue: submissions.queue,
|
|
})
|
|
.from(submissions)
|
|
.innerJoin(characters, eq(submissions.char, characters.id))
|
|
.orderBy(submissions.queue);
|
|
```
|
|
|
|
### Insert
|
|
|
|
```ts
|
|
const [inserted] = await db
|
|
.insert(submissions)
|
|
.values({ name: "Test", uid: "123456789", char: charId })
|
|
.returning({ id: submissions.id, queue: submissions.queue });
|
|
```
|
|
|
|
### Update
|
|
|
|
```ts
|
|
await db
|
|
.update(submissions)
|
|
.set({ checked: true })
|
|
.where(eq(submissions.id, id));
|
|
```
|
|
|
|
### Delete with cascade check (CDN)
|
|
|
|
```ts
|
|
// Use checkCdnRefs() from $api before deleting CDN files
|
|
```
|
|
|
|
### Raw SQL
|
|
|
|
```ts
|
|
import { sql } from "drizzle-orm";
|
|
|
|
await db.execute(
|
|
sql`ALTER SEQUENCE artifact.submissions_queue_seq RESTART WITH 1`,
|
|
);
|
|
```
|
|
|
|
### Transactions
|
|
|
|
```ts
|
|
await db.transaction(async (tx) => {
|
|
await tx.insert(...);
|
|
await tx.update(...);
|
|
});
|
|
```
|
|
|
|
### Counting
|
|
|
|
```ts
|
|
const count = await db.$count(submissions);
|
|
```
|