db-schema-migrations
Skill238 starsChanged 10 days agoMITView on GitHub
How to safely add or modify database schema in Plexus using Drizzle ORM. Use whenever adding tables, columns, or enum values — covers migration rules, schema organization, and dialect-specific timestamp patterns.
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 19
Always edit the correct dialect subdirectory.
- Distinctive: line 27
NEVER modify a live database directly with SQL commands: Always use migrations.
- Very specific: line 34
When authorized to commit, include only the schema .ts changes, not generated SQL, journals, or snapshots.
- Distinctive: line 35
After the PR merges to main, CI auto-generates and commits the migrations.
- Very specific: line 46
If --name is omitted, the script derives a name from the current git branch (e.g., pi/issue-424-1779050379120 → auto_issue_424).
- Very specific: line 50
Use snake_case starting with a verb: add_, create_, drop_, rename_, update_, fix_, remove_, convert_, jsonb_, etc.
- Very specific: line 52
Do not use random or meaningless names.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Database Schema & Migrations
Plexus uses Drizzle ORM with SQLite (default) or Postgres. Schema files live in packages/backend/drizzle/schema/.
Schema Organization
drizzle/schema/
├── index.ts ← must export every table; missing exports = "No schema changes" from drizzle-kit
├── postgres/ ← Postgres table definitions (PLEXUS_DB_TYPE=postgres)
└── sqlite/ ← SQLite table definitions (default)Always edit the correct dialect subdirectory. When adding a new table, update drizzle/schema/index.ts to export it or drizzle-kit generate will silently report "No schema changes."
Migration Rules (non-negotiable)
NEVER edit existing migration files. Migrations represent the historical change sequence. Editing them causes out-of-sync history, data loss risk, and broken deployments.
NEVER manually create migration SQL files or edit meta/_journal.json. Drizzle-kit ignores migrations not in the journal. Manual creation causes conflicting migrations and corrupts the migration system.
NEVER modify a live database directly with SQL commands. Always use migrations.
The Only Correct Workflow
- Edit schema
.tsfiles inpostgres/orsqlite/. - Validate locally (optional): run
bun run generate-migrationsfrom the repository root and inspect the generated SQL. Leave generated artifacts in place and uncommitted; do not delete them. - Never run
drizzle-kit generatedirectly. - When authorized to commit, include only the schema
.tschanges, not generated SQL, journals, or snapshots. The pre-commit hook blocks migration artifacts. - After the PR merges to
main, CI auto-generates and commits the migrations.
Migration Naming
Migrations must have a descriptive, semantic name. The wrapper script enforces this:
bun run generate-migrations # auto-derives name from branch
bun run generate-migrations --name add_quota_checkers # explicit nameIf --name is omitted, the script derives a name from the current git branch (e.g., pi/issue-424-1779050379120 → auto_issue_424). On the main branch, --name is required.
This produces files like 0044_add_quota_checkers.sql or 0044_auto_issue_424.sql instead of 0044_rare_skullbuster.sql.
- Use
snake_casestarting with a verb:add_,create_,drop_,rename_,update_,fix_,remove_,convert_,jsonb_, etc. - Auto-derived names use the
auto_prefix. - Do not use random or meaningless names. The
lint:migrationsscript rejects them.
Bypasses (maintainers only):
- Pre-commit hook:
ALLOW_MIGRATIONS=1 git commit - PR check: add the
migrations-oklabel to the PR
Adding a New Table — Checklist
- Create the table definition in the appropriate dialect directory.
- Export the new table from
drizzle/schema/index.ts. - If Postgres, add any new enum values to
postgres/enums.ts(e.g.quotaCheckerTypeEnum). - If validating locally, run
bun run generate-migrations --name <descriptive-name>from the repository root and inspect the SQL. - Leave generated migration artifacts in place and uncommitted. When authorized to commit, include only the
.tsschema changes.
Type Definitions
Infer types from the schema rather than writing them manually:
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
import { myTable } from '../drizzle/schema';
type MyRow = InferSelectModel<typeof myTable>;
type NewMyRow = InferInsertModel<typeof myTable>;Shared inferred types also live in packages/backend/src/db/types.ts.
Dialect-Aware Timestamp Conversions
SQLite and Postgres use different column types for timestamps. Never write inline dialect === 'postgres' ? ... : ... timestamp logic. Always use the shared utilities from src/utils/normalize.ts.
Pattern 1: text (SQLite) vs timestamp (Postgres)
Used by columns like mcp_request_usage.created_at, mcp_debug_logs.created_at.
- SQLite
text('col')→ Drizzle expects astring(ISO 8601) - Postgres
timestamp('col')→ Drizzle expects aDateobject
import { toDbTimestamp } from '../../utils/normalize';
import { getCurrentDialect } from '../../db/client';
const createdAt = toDbTimestamp(record.created_at, getCurrentDialect());
// SQLite → "2026-02-09T17:36:14.297Z" (string)
// Postgres → Date objectPattern 2: integer(timestamp_ms) (SQLite) vs bigint(number) (Postgres)
Used by columns like quota_snapshots.checked_at, quota_snapshots.resets_at, quota_snapshots.created_at.
- SQLite
integer('col', { mode: 'timestamp_ms' })→ Drizzle expects aDateobject - Postgres
bigint('col', { mode: 'number' })→ Drizzle expects anumber(epoch ms)
import { toDbTimestampMs } from '../../utils/normalize';
import { getCurrentDialect } from '../../db/client';
const checkedAt = toDbTimestampMs(result.checkedAt, getCurrentDialect());
// SQLite → Date object
// Postgres → 1739122574297 (number)Both functions accept Date | number | string | null | undefined and return null for invalid/nullish values.
In this repository10 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- add-provider-presetSkill
- add-quota-checkerSkill
- agent-browserSkill
- batch-grill-meSkill
- frontend-testingSkill
- plexus-cliSkill
- plexus-rest-apiSkill
- to-ticketsSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

