page.tsx230 lines · main
| 1 | import Link from 'next/link'; |
| 2 | |
| 3 | import { DocsShell } from '../../../components/shell'; |
| 4 | |
| 5 | export const metadata = { title: 'migration · drizzle → briven' }; |
| 6 | |
| 7 | export default function DrizzleMigrationPage() { |
| 8 | return ( |
| 9 | <DocsShell> |
| 10 | <p className="font-mono text-xs text-[var(--color-text-muted)]"> |
| 11 | <Link href="/migration" className="hover:text-[var(--color-text)]"> |
| 12 | ← migration |
| 13 | </Link> |
| 14 | </p> |
| 15 | <h1 className="mt-2 font-mono text-2xl tracking-tight">drizzle → briven</h1> |
| 16 | <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]"> |
| 17 | port a drizzle-orm + postgres project onto briven. follow the ten-step playbook on{' '} |
| 18 | <Link href="/migration" className="underline underline-offset-2"> |
| 19 | /migration |
| 20 | </Link>{' '} |
| 21 | — this page covers only the drizzle-specific parts. |
| 22 | </p> |
| 23 | |
| 24 | <div className="mt-4 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]"> |
| 25 | drizzle is the closest source shape to briven's schema dsl — both target postgres |
| 26 | with typescript-first definitions. the schema port is mostly a search/replace; the |
| 27 | functions port is replacing the drizzle <code>db.select(...)</code> chains with briven's |
| 28 | <code> ctx.db(...)</code> chains (same query-builder shape). |
| 29 | </div> |
| 30 | |
| 31 | <Section title="schema port — direct mappings"> |
| 32 | <p>drizzle column helpers map to briven helpers as follows:</p> |
| 33 | <Snippet>{`// drizzle/schema.ts |
| 34 | import { pgTable, text, boolean, timestamp, integer, jsonb } from 'drizzle-orm/pg-core'; |
| 35 | |
| 36 | export const posts = pgTable('posts', { |
| 37 | id: text('id').primaryKey(), |
| 38 | authorId: text('author_id').notNull().references(() => users.id), |
| 39 | title: text('title').notNull(), |
| 40 | published: boolean('published').notNull().default(false), |
| 41 | views: integer('views').notNull().default(0), |
| 42 | metadata: jsonb('metadata').$type<{ tags: string[] }>(), |
| 43 | createdAt: timestamp('created_at').notNull().defaultNow(), |
| 44 | }); |
| 45 | |
| 46 | // briven/schema.ts |
| 47 | import { bigint, boolean, jsonb, schema, table, text, timestamp } from '@briven/cli/schema'; |
| 48 | |
| 49 | export default schema({ |
| 50 | posts: table({ |
| 51 | columns: { |
| 52 | id: text().primaryKey(), |
| 53 | authorId: text().notNull().references('users', 'id'), |
| 54 | title: text().notNull(), |
| 55 | published: boolean().notNull().default('false'), |
| 56 | views: bigint().notNull().default('0'), |
| 57 | metadata: jsonb<{ tags: string[] }>().nullable(), |
| 58 | createdAt: timestamp().notNull().default('now()'), |
| 59 | }, |
| 60 | }), |
| 61 | });`}</Snippet> |
| 62 | <ul className="list-disc pl-5"> |
| 63 | <li> |
| 64 | drizzle <code>integer()</code> → briven <code>bigint()</code> (briven defaults to int8 |
| 65 | for numeric counters to head off overflow). |
| 66 | </li> |
| 67 | <li> |
| 68 | drizzle <code>jsonb<T>().$type<T>()</code> → briven{' '} |
| 69 | <code>jsonb<T>()</code>. type assertion lives on the column builder in both. |
| 70 | </li> |
| 71 | <li> |
| 72 | drizzle <code>.defaultNow()</code> → briven <code>.default('now()')</code>{' '} |
| 73 | (we accept a string-literal sql default; <code>now()</code> is recognised verbatim). |
| 74 | </li> |
| 75 | <li> |
| 76 | drizzle <code>.references(() => users.id)</code> → briven{' '} |
| 77 | <code>.references('users', 'id')</code>. drizzle's closure |
| 78 | form preserves circular-ref ordering; briven resolves by name so the order doesn't |
| 79 | matter. |
| 80 | </li> |
| 81 | <li> |
| 82 | drizzle uses <code>snake_case</code> column names in the second arg; briven derives |
| 83 | the sql name from the property name (camelCase → snake_case) so you can drop the |
| 84 | extra arg. |
| 85 | </li> |
| 86 | </ul> |
| 87 | </Section> |
| 88 | |
| 89 | <Section title="indexes"> |
| 90 | <p> |
| 91 | drizzle defines indexes via the third tuple arg on <code>pgTable</code>. briven uses an |
| 92 | inline <code>indexes</code> array on the table def. |
| 93 | </p> |
| 94 | <Snippet>{`// drizzle |
| 95 | export const posts = pgTable('posts', { /* columns */ }, (t) => ({ |
| 96 | authorIdx: index('posts_author_idx').on(t.authorId), |
| 97 | publishedAuthorIdx: index().on(t.published, t.authorId), |
| 98 | })); |
| 99 | |
| 100 | // briven |
| 101 | posts: table({ |
| 102 | columns: { /* ... */ }, |
| 103 | indexes: [ |
| 104 | { columns: ['authorId'], unique: false }, |
| 105 | { columns: ['published', 'authorId'], unique: false }, |
| 106 | ], |
| 107 | });`}</Snippet> |
| 108 | <p> |
| 109 | drizzle's named indexes don't round-trip — briven generates names from |
| 110 | <code> (table, columns)</code> so renaming a column auto-renames the index too. if a |
| 111 | drizzle index was named for a specific reason (e.g. partial indexes via raw sql), open an |
| 112 | issue; partial indexes are a known gap. |
| 113 | </p> |
| 114 | </Section> |
| 115 | |
| 116 | <Section title="data export from drizzle's postgres"> |
| 117 | <p> |
| 118 | drizzle is just a query builder on top of postgres, so the export is the same as the{' '} |
| 119 | <Link href="/migration/postgres" className="underline underline-offset-2"> |
| 120 | raw-postgres playbook |
| 121 | </Link> |
| 122 | : |
| 123 | </p> |
| 124 | <Snippet>{`pg_dump --format=custom --no-owner --no-privileges \\ |
| 125 | "$DRIZZLE_DATABASE_URL" > drizzle-dump-$(date +%Y%m%d).dump |
| 126 | |
| 127 | pg_restore --no-owner --no-privileges \\ |
| 128 | -d "$BRIVEN_PROJECT_DSN" drizzle-dump-$(date +%Y%m%d).dump`}</Snippet> |
| 129 | <p> |
| 130 | briven's migration applies a clean schema first; <code>pg_restore</code> writes the |
| 131 | data into the same tables. if the drizzle source had columns briven's dsl can't |
| 132 | model yet (e.g. partial indexes, enum types, custom types), restore will warn — fix at |
| 133 | the data level rather than retroactively changing the briven schema. |
| 134 | </p> |
| 135 | </Section> |
| 136 | |
| 137 | <Section title="functions port — query builder is nearly identical"> |
| 138 | <p> |
| 139 | drizzle <code>db</code> and briven <code>ctx.db</code> share the postgres-query-builder |
| 140 | shape (both lean on knex semantics). the port is a search/replace on the import + the |
| 141 | handle name. |
| 142 | </p> |
| 143 | <Snippet>{`// before — drizzle handler |
| 144 | import { db } from './db'; |
| 145 | import { posts, users } from './schema'; |
| 146 | import { and, eq, desc } from 'drizzle-orm'; |
| 147 | |
| 148 | export async function recentPostsByAuthor(authorId: string, limit = 50) { |
| 149 | return db |
| 150 | .select({ id: posts.id, title: posts.title, createdAt: posts.createdAt }) |
| 151 | .from(posts) |
| 152 | .where(and(eq(posts.authorId, authorId), eq(posts.published, true))) |
| 153 | .orderBy(desc(posts.createdAt)) |
| 154 | .limit(limit); |
| 155 | } |
| 156 | |
| 157 | // after — briven/functions/recentPostsByAuthor.ts |
| 158 | import { brivenError, query, type Ctx } from '@briven/cli/server'; |
| 159 | |
| 160 | interface Args { authorId: string; limit?: number } |
| 161 | |
| 162 | export default query(async (ctx: Ctx, args: Args) => { |
| 163 | if (!args.authorId) |
| 164 | throw new brivenError('validation_failed', 'authorId required', { status: 400 }); |
| 165 | return ctx |
| 166 | .db('posts') |
| 167 | .select(['id', 'title', 'createdAt']) |
| 168 | .where({ authorId: args.authorId, published: true }) |
| 169 | .orderBy('createdAt', 'desc') |
| 170 | .limit(Math.min(args.limit ?? 50, 200)); |
| 171 | });`}</Snippet> |
| 172 | <ul className="list-disc pl-5"> |
| 173 | <li> |
| 174 | drizzle <code>db.select({'{a, b}'})</code> → briven{' '} |
| 175 | <code>ctx.db('table').select(['a', 'b'])</code>. |
| 176 | </li> |
| 177 | <li> |
| 178 | drizzle's <code>and / eq / desc</code> operators → briven uses object-literal |
| 179 | where clauses + string-keyed <code>orderBy</code> (knex-style). drizzle's richer |
| 180 | operators (e.g. <code>ilike</code>, <code>arrayContains</code>) land on briven via |
| 181 | raw fragments — see <Link href="/functions" className="underline underline-offset-2">/functions</Link>. |
| 182 | </li> |
| 183 | <li> |
| 184 | drizzle <code>insert / update / delete</code> → briven{' '} |
| 185 | <code>ctx.db(table).insert / update / delete</code> — same shape. |
| 186 | </li> |
| 187 | </ul> |
| 188 | </Section> |
| 189 | |
| 190 | <Section title="auth port"> |
| 191 | <p> |
| 192 | drizzle ships no auth — you're running it next to better-auth, lucia, next-auth, or |
| 193 | a hand-rolled session table. briven ships better-auth integrated; map your existing user |
| 194 | / session columns into briven's control-plane shape via the{' '} |
| 195 | <Link href="/migration/nextauth" className="underline underline-offset-2"> |
| 196 | nextauth → briven |
| 197 | </Link>{' '} |
| 198 | guide (the same column-mapping applies whichever lib generated the rows). |
| 199 | </p> |
| 200 | </Section> |
| 201 | |
| 202 | <Section title="reactivity (new capability)"> |
| 203 | <p> |
| 204 | drizzle queries are one-shot. once on briven, wrap a read as a <code>query()</code> and |
| 205 | the same call from <code>@briven/react</code>'s <code>useQuery</code> auto-refetches |
| 206 | on table-level NOTIFYs. no extra code on the function side. |
| 207 | </p> |
| 208 | </Section> |
| 209 | </DocsShell> |
| 210 | ); |
| 211 | } |
| 212 | |
| 213 | function Section({ title, children }: { title: string; children: React.ReactNode }) { |
| 214 | return ( |
| 215 | <section className="mt-10"> |
| 216 | <h2 className="font-mono text-lg">{title}</h2> |
| 217 | <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]"> |
| 218 | {children} |
| 219 | </div> |
| 220 | </section> |
| 221 | ); |
| 222 | } |
| 223 | |
| 224 | function Snippet({ children }: { children: string }) { |
| 225 | return ( |
| 226 | <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs"> |
| 227 | <code>{children}</code> |
| 228 | </pre> |
| 229 | ); |
| 230 | } |