Drizzle ORM patterns: schema definitions, Drizzle Kit migrations, query builders, type branding, custom types, SQLite, Postgres, D1, and Turso/libSQL boundaries. Use when mentioning Drizzle, drizzle-orm, DB schemas, migrations, branded column types, or typed SQL queries.
76
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
When Drizzle schema definitions, migration snapshots, query builder APIs, column typing, custom types, or driver integration affect correctness, use source-backed grounding before relying on memory. If DeepWiki MCP is available, ask a narrow question against drizzle-team/drizzle-orm; for libSQL, Turso sync, embedded replicas, D1 compatibility, or remote SQLite behavior, ask against tursodatabase/turso. If DeepWiki is unavailable or the repo is not indexed, use upstream source or official docs directly. Treat DeepWiki as orientation, then verify decisive details against local installed types, generated migrations, source, driver versions, or official docs before changing code.
Skip DeepWiki for repo-local schema naming and storage-boundary conventions already documented below.
drizzle(...) and Drizzle Kit config.drizzle-zod, drizzle-valibot, or a local schema parser at IO boundaries when external input becomes a row. Do not treat inferred insert types as runtime validation.bun:sqlite and better-sqlite3 are local synchronous SQLite drivers. Do not use them in Cloudflare Workers.turso skill for those decisions.When you need a column with a branded TypeScript type but no actual data transformation, use $type<T>() instead of customType.
If toDriver and fromDriver would be identity functions (x) => x, use $type<T>() instead.
Even with identity functions, customType still invokes mapFromDriverValue on every row:
// drizzle-orm/src/utils.ts - runs for EVERY column of EVERY row
const rawValue = row[columnIndex]!;
const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);Query 1000 rows with 3 date columns = 3000 function calls doing nothing.
// Runtime overhead for identity functions
customType<{ data: DateTimeString; driverParam: DateTimeString }>({
dataType: () => 'text',
toDriver: (value) => value, // called on every write
fromDriver: (value) => value, // called on every read
});// Zero runtime overhead - pure type assertion
text().$type<DateTimeString>();$type<T>() is a compile-time-only type override:
// drizzle-orm/src/column-builder.ts
$type<TType>(): $Type<this, TType> {
return this as $Type<this, TType>;
}Only when data genuinely transforms between app and database:
// JSON: object ↔ string - actual transformation
customType<{ data: UserPrefs; driverParam: string }>({
toDriver: (value) => JSON.stringify(value),
fromDriver: (value) => JSON.parse(value),
});Prefer keeping data serialized (strings) through the system, parsing only at the edges (UI components).
The principle: If data enters serialized and leaves serialized, keep it serialized in the middle. Parse at the edges where you actually need the rich representation.
Instead of parsing DateTimeString into Temporal.ZonedDateTime at the database layer:
// Bad: parse on every read, re-serialize at API boundaries
customType<{ data: Temporal.ZonedDateTime; driverParam: string }>({
fromDriver: (value) => fromDateTimeString(value),
});Keep it as a string until the UI actually needs it:
// Good: string stays string, parse only in date-picker component
text().$type<DateTimeString>();
// In UI component:
const temporal = fromDateTimeString(row.createdAt);
// After edit:
const updated = toDateTimeString(temporal);cb12bcc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.