page.tsx230 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../../components/shell';
4
5export const metadata = { title: 'migration · drizzle → briven' };
6
7export 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&apos;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&apos;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
34import { pgTable, text, boolean, timestamp, integer, jsonb } from 'drizzle-orm/pg-core';
35
36export 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
47import { bigint, boolean, jsonb, schema, table, text, timestamp } from '@briven/cli/schema';
48
49export 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&lt;T&gt;().$type&lt;T&gt;()</code> → briven{' '}
69 <code>jsonb&lt;T&gt;()</code>. type assertion lives on the column builder in both.
70 </li>
71 <li>
72 drizzle <code>.defaultNow()</code> → briven <code>.default(&apos;now()&apos;)</code>{' '}
73 (we accept a string-literal sql default; <code>now()</code> is recognised verbatim).
74 </li>
75 <li>
76 drizzle <code>.references(() =&gt; users.id)</code> → briven{' '}
77 <code>.references(&apos;users&apos;, &apos;id&apos;)</code>. drizzle&apos;s closure
78 form preserves circular-ref ordering; briven resolves by name so the order doesn&apos;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
95export 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
101posts: table({
102 columns: { /* ... */ },
103 indexes: [
104 { columns: ['authorId'], unique: false },
105 { columns: ['published', 'authorId'], unique: false },
106 ],
107});`}</Snippet>
108 <p>
109 drizzle&apos;s named indexes don&apos;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
127pg_restore --no-owner --no-privileges \\
128 -d "$BRIVEN_PROJECT_DSN" drizzle-dump-$(date +%Y%m%d).dump`}</Snippet>
129 <p>
130 briven&apos;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&apos;s dsl can&apos;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
144import { db } from './db';
145import { posts, users } from './schema';
146import { and, eq, desc } from 'drizzle-orm';
147
148export 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
158import { brivenError, query, type Ctx } from '@briven/cli/server';
159
160interface Args { authorId: string; limit?: number }
161
162export 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(&apos;table&apos;).select([&apos;a&apos;, &apos;b&apos;])</code>.
176 </li>
177 <li>
178 drizzle&apos;s <code>and / eq / desc</code> operators → briven uses object-literal
179 where clauses + string-keyed <code>orderBy</code> (knex-style). drizzle&apos;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&apos;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&apos;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>&apos;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
213function 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
224function 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}