Skip to content

Commit

Permalink
Merge pull request #157 from eoin-obrien/read-replicas
Browse files Browse the repository at this point in the history
read replicas
  • Loading branch information
eoin-obrien authored Oct 23, 2024
2 parents d312d2e + 16f3c83 commit 357f3ec
Show file tree
Hide file tree
Showing 10 changed files with 3,513 additions and 0 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,57 @@ generator kysely {

Take a look at [the camel case example](examples/camel-case/) to see it in action! Check out the [Kysely documentation](https://kysely.dev/) for more information about plugins.

## Read Replicas

Using read replicas with `prisma-extension-kysely` is a breeze!
Just use the excellent [`@prisma/extension-read-replicas`](https://www.npmjs.com/package/@prisma/extension-read-replicas) extension as normal.
Pay attention to how it's configured, though:

```typescript
// Use a common config for primary and replica clients (or different configs)
const kyselyExtensionArgs: PrismaKyselyExtensionArgs<DB> = {
kysely: (driver) =>
new Kysely<DB>({
dialect: {
createAdapter: () => new SqliteAdapter(),
createDriver: () => driver,
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler(),
},
}),
};

// Initialize the replica client(s) and add the Kysely extension
const replicaClient = new PrismaClient({
datasourceUrl: "YOUR_REPLICA_URL", // Replace this with your replica's URL!
log: [{ level: "query", emit: "event" }],
}).$extends(kyselyExtension(kyselyExtensionArgs));

// Initialize the primary client and add the Kysely extension and the read replicas extension
const prisma = new PrismaClient()
.$extends(kyselyExtension(kyselyExtensionArgs)) // Apply the Kysely extension before the read replicas extension!
.$extends(
readReplicas({
replicas: [replicaClient],
}),
); // Apply the read replicas extension after the Kysely extension!
```

See how we're setting up the replica client as a fully-fledged Prisma client and extending it separately? That's the secret sauce!
It make sure that the replica client has a separate Kysely instance. If you try to use bare URLs, you'll run into trouble;
it'll share the same Kysely instance as the primary client, and you'll get unpleasant surprises!

```typescript
// Don't do this! It won't work as expected.
readReplicas({
url: "postgresql://user:password@localhost:5432/dbname",
});
```

Also, note that we're applying the Kysely extension before the read replicas extension. This is important! If you apply the read replicas extension first, you won't get `.$kysely` on the primary client.

Check out the [read replicas example](examples/read-replicas/) for a runnable example!

## Examples

Check out the [examples](examples) directory for a sample project!
Expand Down
5 changes: 5 additions & 0 deletions examples/read-replica/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated files
prisma/generated

# Prisma local database file
prisma/dev.db
122 changes: 122 additions & 0 deletions examples/read-replica/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { PrismaClient } from "@prisma/client";
import {
Kysely,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler,
} from "kysely";
import kyselyExtension, {
PrismaKyselyExtensionArgs,
} from "prisma-extension-kysely";
import type { DB } from "./prisma/generated/types.js";
import { readReplicas } from "@prisma/extension-read-replicas";

// Use a common config for primary and replica clients
const kyselyExtensionArgs: PrismaKyselyExtensionArgs<DB> = {
kysely: (driver) =>
new Kysely<DB>({
dialect: {
createAdapter: () => new SqliteAdapter(),
createDriver: () => driver,
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler(),
},
}),
};

// Initialize the replica client(s) and add the Kysely extension
const replicaClient = new PrismaClient({
// For demonstration purposes, use the same SQLite database
datasourceUrl: "file:./dev.db",
log: [{ level: "query", emit: "event" }],
}).$extends(kyselyExtension(kyselyExtensionArgs));

// Initialize the primary client and add the Kysely extension
const prisma = new PrismaClient()
.$extends(kyselyExtension(kyselyExtensionArgs))
.$extends(
readReplicas({
replicas: [replicaClient],
}),
);

async function main() {
console.log(
"Is prisma.$kysely the same as prisma.$primary().$kysely?",
prisma.$kysely === prisma.$primary().$kysely,
);
console.log(
"Is prisma.$kysely the same as prisma.$replica().$kysely?",
prisma.$kysely === prisma.$replica().$kysely,
);
console.log(
"Is prisma.$primary().$kysely the same as prisma.$replica().$kysely?",
prisma.$primary().$kysely === prisma.$replica().$kysely,
);

// Clear the database before running the example
const deletedPosts = await prisma.$kysely
.deleteFrom("Post")
.executeTakeFirstOrThrow();
console.log("Deleted posts:", Number(deletedPosts.numDeletedRows));
const deletedUsers = await prisma.$kysely
.deleteFrom("User")
.executeTakeFirstOrThrow();
console.log("Deleted users:", Number(deletedUsers.numDeletedRows));

// Create and update a user
const insertedUser = await prisma.$transaction(async (tx) => {
const [insertedUser] = await tx.$kysely
.insertInto("User")
.values({ name: "John", email: "[email protected]" })
.returningAll()
.execute();
const affectedRows = await tx.$kysely
.updateTable("User")
.set({ name: "John Doe" })
.where("id", "=", insertedUser.id)
.executeTakeFirstOrThrow();
console.log("Updated users:", Number(affectedRows.numUpdatedRows));

return insertedUser;
});

// Create a post
await prisma
.$primary()
.$kysely.insertInto("Post")
.values({
title: "Hello prisma!",
content: "This is my first post about prisma",
updatedAt: new Date().toISOString(),
published: 1,
authorId: insertedUser.id,
})
.returningAll()
.execute();

// Select a user and a post
const userQuery = prisma
.$replica()
.$kysely.selectFrom("User")
.selectAll()
.where("id", "=", insertedUser.id);
const user = await userQuery.executeTakeFirstOrThrow();

const postQuery = prisma
.$replica()
.$kysely.selectFrom("Post")
.selectAll()
.where((eb) =>
eb.or([
eb("title", "like", "%prisma%"),
eb("content", "like", "%prisma%"),
]),
)
.where("published", "=", 1);
const post = await postQuery.executeTakeFirstOrThrow();

console.log("Query results:", { user, post });
}

main();
Loading

0 comments on commit 357f3ec

Please sign in to comment.