Joins [SQL]
Join clause in SQL is used to combine 2 or more tables, based on related columns between them. Drizzle ORM joins syntax is a balance between the SQL-likeness and type safety.
Join types
Drizzle ORM has APIs for INNER JOIN
, FULL JOIN
, LEFT JOIN
and RIGHT JOIN
Lets have a quick look at examples based on below table schemas:
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
export const pets = pgTable('pets', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
ownerId: integer('owner_id').notNull().references(() => users.id),
})
Left Join
const result = await db.select().from(users).leftJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" left join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
user: {
id: number;
name: string;
};
pets: {
id: number;
name: string;
ownerId: number;
} | null;
}[];
Right Join
const result = await db.select().from(users).rightJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" right join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
user: {
id: number;
name: string;
} | null;
pets: {
id: number;
name: string;
ownerId: number;
};
}[];
Inner Join
const result = await db.select().from(users).innerJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" inner join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
user: {
id: number;
name: string;
};
pets: {
id: number;
name: string;
ownerId: number;
};
}[];
Full Join
const result = await db.select().from(users).fullJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" full join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
user: {
id: number;
name: string;
} | null;
pets: {
id: number;
name: string;
ownerId: number;
} | null;
}[];
Partial select
If you need to select a particular subset of fields or to have a flat response type - Drizzle ORM
supports joins with partial select and will automatically infer return type based on .select({ ... })
structure
await db.select({
userId: users.id,
petId: pets.is,
}).from(user).leftJoin(pets, eq(users.id, pets.ownerId))
select "users"."id", "pets","id" from "users" left join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
userId: number;
petId: number | null;
}[];
You might've noticed that petId
can be null now, it's because we're left joining and there can be users without a pet.
It's very important to keep in mind when using sql
operator for partial selection fields and aggregations when needed,
you should to use sql<type | null>
for proper result type inference, that one is on you!
const result = await db.select({
userId: users.id,
petId: pets.id,
petName1: sql`upper(${pets.name})`,
petName2: sql<string | null>`upper(${pets.name})`,
//˄we should explicitly tell 'string | null' in type, since we're left joining that field
}).from(user).leftJoin(pets, eq(users.id, pets.ownerId))
select "users"."id", "pets"."id", upper("pets"."name")... from "users" left join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
userId: number;
petId: number | null;
petName1: unknown;
petName2: string | null;
}[];
To avoid plethora of nullable fields when joining tables with lots of columns - we can utilise our nested select object syntax Our smart type inference will make whole object nullable instead of making all table fields nullable!
await db.select({
userId: users.id,
userName: users.name,
pet: {
id: pets.id,
name: pets.name,
upperName: sql<string>`upper(${pets.name})`
}
}).from(user).fullJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" full join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
userId: number | null;
userName: string | null;
pet: {
id: number;
name: string;
upperName: string;
} | null;
}[];
Aliases & Selfjoins
Drizzle ORM supports table aliases which comes really handy when you need to do selfjoins Lets say you need to fetch users with their parents
import { user } from "./schema";
const parent = alias(user, "parent")
const result = db
.select()
.from(user)
.leftJoin(parent, eq(parent.id, user.id));
select ... from "user" left join "user" "parent" on "parent"."id" = "user"."id"
// result type
const result: {
user: {
id: number;
name: string;
parentId: number;
};
parent: {
id: number;
name: string;
parentId: number;
} | null;
}[];
Aggregating results
Drizzle ORM delivers name-mapped results from the driver without changing the structure. You're free to operate with results the way you want and here's an example of mapping many-one relational data
type User = InferModel<typeof users>;
type Pet = InferModel<typeof pets>;
const rows = db.select({
user: users,
pet: pets,
}).from(users).leftJoin(pets, eq(users.id, pets.ownerId)).all();
const result = rows.reduce<Record<number, { user: User; pets: Pet[] }>>(
(acc, row) => {
const user = row.user;
const pet = row.pet;
if (!acc[user.id]) {
acc[user.id] = { user, pets: [] };
}
if (pet) {
acc[user.id].pets.push(pet);
}
return acc;
},
{}
);
// result type
const result: Record<number, {
user: User;
pets: Pet[];
}>;
Many-to-one example
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { drizzle } from 'drizzle-orm/better-sqlite3';
const cities = sqliteTable('cities', {
id: integer('id').primaryKey(),
name: text('name'),
});
const users = sqliteTable('users', {
id: integer('id').primaryKey(),
name: text('name'),
cityId: integer('city_id').references(() => cities.id)
});
const db = drizzle(sqlite);
const result = db.select().from(cities).leftJoin(users, eq(cities2.id, users2.cityId)).all();
Many-to-many example
const users = sqliteTable('users', {
id: integer('id').primaryKey(),
name: text('name'),
});
const chatGroups = sqliteTable('chat_groups', {
id: integer('id').primaryKey(),
name: text('name'),
});
const usersToChatGroups = sqliteTable('usersToChatGroups', {
userId: integer('user_id').notNull().references(() => users.id),
groupId: integer('group_id').notNull().references(() => chatGroups.id),
});
// querying user group with id 1 and all the participants(users)
db.select()
.from(usersToChatGroups)
.leftJoin(users, eq(usersToChatGroups.userId, users.id))
.leftJoin(chatGroups, eq(usersToChatGroups.groupId, chatGroups.id))
.where(eq(chatGroups.id, 1))
.all();