April 03, 2025

Hey Drizzle, please help — we want to speak SQL

Let’s start with the obvious: SQL isn’t going anywhere. It’s expressive, powerful, and battle-tested. But historically, the developer experience around it hasn’t always been great. You’re often stuck choosing between raw SQL, which can be brittle and hard to manage, or ORMs that add so much abstraction you sometimes lose sight of the underlying SQL — especially when things get complex.

At Gel, we’ve always believed in meeting developers where they are without compromising on power or clarity. We created EdgeQL, our own query language: composable, hierarchical, type-safe. But we also knew that EdgeQL has a learning curve. And for many developers, the first question is: "Can I use SQL?"

And once we've built enough internal infrastructure to support SQL we've decided: it's time.

So we started exploring how to bring SQL to Gel. Around that time, we started talking to the Drizzle team — and it quickly became clear they were the right partners for this journey.

We wanted to support SQL workflows in a way that felt modern. And Drizzle just clicked. Drizzle is SQL-centric, with just enough abstraction to feel ergonomic — but never out of touch with your queries. Type-safe, clear, and plays well with Postgres. But more than that, the Drizzle team felt like a kindred spirits.

As we started integrating SQL into Gel and working with the Drizzle team, it became clear that their focus on clarity and explicitness aligned well with our own approach. So we sponsored Drizzle. And then we partnered with them. And that’s when the real fun began.

Funny enough, our hooks system was directly inspired by our early work with Drizzle. It was a feature we added to make the developer workflow smoother — especially so that drizzle-kit pull could run automatically after a Gel migration. Here's an example of how you can use it:

Copy
[instance]
server-version = "6.4"

[hooks]
schema.update.after = """
npx drizzle-kit pull
"""

We didn’t just slap a Postgres layer on and call it a day. From the beginning, this was about designing a real developer experience together — one that works for how people actually build applications.

One of the earliest discussions we had was about whether we should simply use the PostgreSQL dialect in Drizzle or introduce a dedicated "gel" dialect. Initially, we confirmed that using the Postgres protocol to introspect the database was working reasonably well. But there were obvious mismatches — like internal fields (__type__, autogenerated id columns) and schema artifacts that didn't translate cleanly.

The Drizzle team proposed to create a dedicated Gel dialect inside Drizzle ORM and Drizzle Kit, complete with a custom gelTable() API and Gel-native introspection logic using EdgeQL.

This led to the plan of building a full Gel driver and a dialect in Drizzle that talks to Gel over our native protocol. This also meant we could support .querySQL(...) via gel-js, enabling raw SQL queries with parameters and raw result formats, which the Drizzle team requested for full parity.

The collaboration brought huge benefits on both sides. While we were adapting Gel to support Drizzle's expectations, the Drizzle team dove deep into our database behavior and surfaced a variety of edge cases, bugs, and clarifications. Their SQL test suite ended up being a great help for our SQL work. They even created a Notion workspace just to track things they uncovered — from SQL syntax issues and type handling edge cases to nuanced behaviors around onConflict, DISTINCT, subqueries, and more.

Fixing those issues directly improved our SQL layer. They stress-tested Gel's SQL implementation quite considerably.

One particular example? This PR introduced a customizable way to unpack SQL rows from querySQL. Another one added the withCodecs method — allowing developers to customize how specific Gel types are serialized and deserialized when using gel-js. It’s especially useful for handling user-defined scalar types and aligning client-side representations with app logic, and it came directly out of our type alignment conversations with the Drizzle team.

At one point, while the Drizzle team was recording a demo video, they hit a weird bug: everything worked until they added a second migration. Suddenly, Drizzle couldn't introspect the schema — an internal codec error exploded.

Within hours, we were debugging it together in Slack. Turns out it was a regression in a recent beta — something deep in how we mapped Postgres OIDs over our native protocol.

Yury dove in, the Drizzle team tested various Gel versions, and we shipped a fix in a brand-new RC the next day. And the demo video? Finished right after!

After a few calls, shared Notion docs, and various Slack threads, we landed on a robust integration path that gives developers flexibility and power.

Whether you want the full Gel experience — with its custom query language, rich type system, and advanced access controls — or prefer a more traditional SQL approach that treats Gel like a Postgres-compatible engine, the integration covers both ends of the spectrum.

The recommended approach is to use the new gel dialect in Drizzle, designed to support everything Gel brings to the table. It covers:

  • The gel dialect being covered in drizzle-orm and drizzle-kit, purpose-built to expose both standard PostgreSQL functionality and everything extra Gel has to offer.

  • Support for Gel’s extended type system.

  • Automatic schema pulling with drizzle-kit pull that reflects Gel types into usable Drizzle schemas.

  • Native support for the gel-js client.

Here’s what using Drizzle with Gel looks like in practice:

1. Define your schema in Gel:

Copy
module default {
    type User {
        required name: str;
        required email: str;
        age: int16;
    }
}

2. Setup hooks in your ``gel.toml`` file:

Copy
[instance]
server-version = "6.4"

[hooks]
schema.update.after = """
npx drizzle-kit pull
"""

3. Run migrations:

Copy
gel migration create
gel migration apply

4. Example of the generated Drizzle schema:

Copy
import { gelTable, uniqueIndex, uuid, text } from "drizzle-orm/gel-core"
import { sql } from "drizzle-orm"

export const user = gelTable("User", {
    id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),
    email: text().notNull(),
    identityId: uuid("identity_id").notNull(),
    username: text().notNull(),
}, (table) => [
    uniqueIndex("a033fd76-f37d-11ef-be13-8392c504f254;schemaconstr").using("btree", table.id.asc().nullsLast().op("uuid_ops")),
]);

5. Use Drizzle in your app:

Copy
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/gel";
import { createClient } from "gel";
import { user } from "../drizzle/schema";

const gelClient = createClient();
const db = drizzle({ client: gelClient });

async function main() {
  const newUser: typeof user.$inferInsert = {
    name: "John",
    email: "john@example.com",
    age: 30,
  };

  await db.insert(user).values(newUser);
  const users = await db.select().from(user);
  console.log(users);
}

Check out the Drizzle docs to learn more about using Gel with Drizzle. And if you’re already a Drizzle user, give Gel a try. We think you’ll like what you see.

We'll be sharing more on the Drizzle integration in the coming weeks. Stay tuned for deep dives, tutorials, and more.

This was a practical collaboration. We needed solid SQL support in Gel, and Drizzle helped us get there — quickly, and with helpful, actionable feedback along the way.

If you're using Gel, and you like Drizzle, this just works now.

Give it a try!

ShareTweet