agentleFS
mcowgermcowger/plexus/.agents/skills/db-schema-migrations/SKILL.md

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
  1. Very specific:

    Always edit the correct dialect subdirectory.

    line 19
  2. Distinctive:

    NEVER modify a live database directly with SQL commands: Always use migrations.

    line 27
  3. Very specific:

    When authorized to commit, include only the schema .ts changes, not generated SQL, journals, or snapshots.

    line 34
  4. Distinctive:

    After the PR merges to main, CI auto-generates and commits the migrations.

    line 35
  5. Very specific:

    If --name is omitted, the script derives a name from the current git branch (e.g., pi/issue-424-1779050379120 → auto_issue_424).

    line 46
  6. Very specific:

    Use snake_case starting with a verb: add_, create_, drop_, rename_, update_, fix_, remove_, convert_, jsonb_, etc.

    line 50
  7. Very specific:

    Do not use random or meaningless names.

    line 52

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  1. Edit schema .ts files in postgres/ or sqlite/.
  2. Validate locally (optional): run bun run generate-migrations from the repository root and inspect the generated SQL. Leave generated artifacts in place and uncommitted; do not delete them.
  3. Never run drizzle-kit generate directly.
  4. When authorized to commit, include only the schema .ts changes, not generated SQL, journals, or snapshots. The pre-commit hook blocks migration artifacts.
  5. 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 name

If --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_case starting 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:migrations script rejects them.

Bypasses (maintainers only):

  • Pre-commit hook: ALLOW_MIGRATIONS=1 git commit
  • PR check: add the migrations-ok label 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 .ts schema 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 a string (ISO 8601)
  • Postgres timestamp('col') → Drizzle expects a Date object
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 object

Pattern 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 a Date object
  • Postgres bigint('col', { mode: 'number' }) → Drizzle expects a number (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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.