# Migrating from raw Drizzle

> A practical path from hand-written Drizzle query glue to better-drizzle delegates.

URL: https://better-drizzle.com/docs/guides/migrating-from-drizzle

The easiest migration is incremental.

## 1. Keep your existing schema

Do not rewrite tables. better-drizzle reads tables and relations from the Drizzle instance, so declare relations with Drizzle ORM 1.x `defineRelations(schema, ...)` and pass them to `drizzle()`. With no relations, use `defineRelations(schema)`.

## 2. Wrap the existing Drizzle client once

**PostgreSQL**

```ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/node-postgres'; // Or your driver
import { relations } from './relations';

const db = drizzle({ client: connection, relations });
export const client = better(db);
```

**MySQL**

```ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/mysql2'; // Or your driver
import { relations } from './relations';

const db = drizzle({ client: connection, relations });
export const client = better(db);
```

**SQLite**

```ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/bun-sqlite'; // Or your driver
import { relations } from './relations';

const db = drizzle({ client: connection, relations });
export const client = better(db);
```

## 3. Move repetitive read paths first

Good first candidates:

* point lookups
* list pages with `include`
* counts and existence checks
* pagination endpoints
* nested relation filters

These usually deliver the highest reduction in repeated service-layer code.

## 4. Replace branchy write glue with direct writes



**better-drizzle**

```ts  
const maybeCreated = await client.users.create({
	data: {
		email: 'alice@example.com',
		name: 'Alice',
	},
	skipDuplicates: ['email'],
});
```

**Drizzle**

```ts  
const [inserted] = await db
	.insert(users)
	.values({
		email: 'alice@example.com',
		name: 'Alice',
	})
	.onConflictDoNothing({ target: users.email })
	.returning();
const maybeCreated = inserted ?? null;
```

And for bulk sync paths:



**better-drizzle**

```ts  
await client.users.upsertMany({
	data,
	target: ['email'],
	update: ['name', 'active'],
});
```

**Drizzle**

```ts  
import { sql } from 'drizzle-orm';

await db
	.insert(users)
	.values(data)
	.onConflictDoUpdate({
		target: users.email,
		set: {
			name: sql`excluded.name`,
			active: sql`excluded.active`,
		},
	});
```

## 5. Move cross-cutting behavior into plugins or hooks

If you currently repeat logic like:

* timestamps
* soft delete visibility
* audit metadata
* trace IDs
* authorization checks

those are usually better expressed once through plugins or hooks.

## Raw Drizzle vs better-drizzle

Keep raw Drizzle when:

* the query is truly custom SQL
* you need a database-specific function or reporting query
* the delegate shape would be more awkward than the SQL

Use better-drizzle when:

* you're rebuilding CRUD and query glue by hand
* you want predictable return shapes
* you want typed relation filters and includes without repeating join code

## Migration strategy that works well

1. wrap the client
2. migrate one service or endpoint at a time
3. keep raw SQL where it is the clearest tool
4. introduce plugins only after the base delegate API is in use
