Skip to main content
PrismaPrisma

Prisma

prisma-postgrejs is a Prisma driver adapter for postgrejs. Swap it in where @prisma/adapter-pg goes and everything above it stays the same — your schema, your queries, your migrations.

On large values it is faster and asks the runtime for much less memory. A 4MB bytea comes back in 50.756 ms against 108.827 ms, and one call allocates 21.6 MB against 69.2 MB. pg asks for that column as \x-prefixed hex, two characters a byte, so it pulls 8.0 MB off the socket where this adapter pulls 4.0 MB, then holds it as a string off the JS heap where a heap figure alone can't see it. An int4[] of 100k values runs 3.28x faster, at 13.4 MB against 27.3 MB. Ordinary queries gain less, but repeatably: a point read is the faster of the two in 99 of 101 alternated pairs. All of it was measured through an unmodified PrismaClient, against @prisma/adapter-pg on the same server in the same run. Two values the reference adapter gets silently wrong also come back right.

Installation​

npm install prisma-postgrejs postgrejs @prisma/driver-adapter-utils

postgrejs (>=3.11.0 <4) and @prisma/driver-adapter-utils (>=7.10.0 <9) are peer dependencies — the second one because @prisma/client doesn't bring it along. Node >=22.

Quick start​

At Prisma 7 the adapter is how a client connects, so the datasource block carries no url:

datasource db {
provider = "postgresql"
}
import { PrismaClient } from './generated/prisma/client.js';
import { PrismaPostgreJS } from 'prisma-postgrejs';

const adapter = new PrismaPostgreJS('postgresql://user:secret@localhost:5432/mydb');
const prisma = new PrismaClient({ adapter });

await prisma.user.findMany({ where: { active: true } });

That's the whole change.

Usage​

Connecting​

Three ways to say where the database is:

new PrismaPostgreJS('postgresql://user:secret@localhost:5432/mydb'); // a connection string
new PrismaPostgreJS({ host: 'localhost', database: 'mydb', max: 10 }); // postgrejs's pool options
new PrismaPostgreJS(pool); // a Pool you made yourself

prisma.$disconnect() closes a pool this package opened. A Pool passed in stays yours — it's left open, so it can go on serving whatever else is using it.

Options​

new PrismaPostgreJS('postgresql://user:secret@localhost:5432/mydb', {
schema: 'my_schema',
pipeline: true,
onPoolError: err => logger.error(err),
});
OptionDefaultWhat it does
schema?schema= on the connection stringThe schema the engine qualifies its SQL with
pipelinetrueLets a statement share a pooled connection with statements already in flight instead of waiting for one of its own — this is what the concurrency rows below measure. false gives each statement the connection to itself
onPoolErrorCalled when a pooled connection is lost or one couldn't be opened. Prisma has nowhere to report either, so without this they're only visible as the failure of whatever query was in flight

Migrations​

prisma migrate and prisma db push connect with the CLI's own built-in connector rather than through the adapter, so they need a URL of their own in prisma.config.ts:

import { defineConfig } from 'prisma/config';

export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: { url: process.env.DATABASE_URL },
});

Why​

It's a drop-in swap for @prisma/adapter-pg: the same PrismaClient({ adapter }), the same schema, the same queries, the same migrations. The differences:

  • It is faster where the payload is large: 2.14x on a 4MB bytea, and 3.28x on a 100k-element int4[] whose values use the whole type, because the values arrive in PostgreSQL's binary format and are not parsed from text.
  • It allocates less on the same rows: one call allocates 21.6 MB against 69.2 MB on that bytea, and 984 KB against 3.6 MB on an array of 5000 float8. On rows too narrow to carry a payload the two are within a few percent, and on the smallest of them this adapter asks for slightly more.
  • It is slightly faster on ordinary round trips, repeatably, because statements are prepared and reused without anyone asking for it.
  • Two values the reference adapter gets wrong, timetz on any offset but zero and money outside a narrow range, come back right. What changes when you switch is the full list.
  • The client underneath can do what Prisma has no way to ask for: cursors, COPY, LISTEN/NOTIFY, large objects, logical replication and pipelining, on the same pool your queries use.
  • It is checked against Prisma's own functional suite: 1251 of its tests pass, with @prisma/adapter-pg run over the same server in the same invocation as the control.
Workload@prisma/adapter-pgprisma-postgrejsSpeedup / memory
Primary-key lookup0.326 ms / 59 KB0.289 ms / 63 KB1.13x, +6%
10k rows, mixed scalars25.165 ms / 13.2 MB17.019 ms / 10.7 MB1.48x, −19%
10k rows, int8/numeric/timestamp27.552 ms / 17.0 MB20.146 ms / 14.9 MB1.37x, −12%
Point read0.641 ms / 65 KB0.560 ms / 69 KB1.15x, +7%
Page of 2001.845 ms / 582 KB1.506 ms / 480 KB1.23x, −17%
uuid of 5k rows6.591 ms / 3.4 MB5.149 ms / 3.3 MB1.28x, −3%
float8 of 5k rows6.131 ms / 3.4 MB4.420 ms / 3.2 MB1.39x, −4%
float8[] of 5k in one row5.819 ms / 3.6 MB1.865 ms / 984 KB3.12x, −73%
int4[] of 100k in one row64.125 ms / 27.3 MB19.561 ms / 13.4 MB3.28x, −51%
bytea of 4MB108.827 ms / 69.2 MB50.756 ms / 21.6 MB2.14x, −69%
20 inserts in one transaction16.008 ms / 1.1 MB14.091 ms / 1.1 MB1.14x, +6%
32 concurrent count(), pool of 416.786 ms / 1.3 MB10.173 ms / 1.3 MB1.65x, +3%
32 concurrent findFirst, pool of 411.908 ms / 1.6 MB4.454 ms / 1.6 MB2.67x, −3%
32 concurrent findMany, pool of 410.062 ms / 1.3 MB4.266 ms / 1.3 MB2.36x, −3%

@prisma/client 7.10.0, @prisma/adapter-pg 7.10.0, postgrejs 3.12.1, pg 8.23.0, PostgreSQL 18.6, Node 24.15.0, on loopback. Medians per call; the second figure in each cell is what one call asks the runtime for (heapUsed and external together).

How much is gained depends on the shape of the workload, not its size. The same 5000 float8 values read as 5000 rows gain 1.39x; packed into one array column in one row, they gain 3.12x. Both adapters pay the protocol's per-row cost, so binary decoding gains in proportion to how wide the rows are. Large payloads gain most: 2.14x on a 4MB bytea, and 3.28x on an int4[] of 100k values that use the whole type. With more concurrent queries than pool connections the gain is up to 2.67x, and that is the state a web application under load runs in.

How the numbers were measured​

Both adapters run in one process and alternate on every iteration, so neither gets a warmer machine than the other. Each figure is the median of 101 iterations, or 61 where one of them costs more. Memory is a pass of its own, one child process per adapter, so what a client allocates once and keeps is inside the window rather than under it. Everything above is through a real PrismaClient.

The medians alone wouldn't be worth much — this was a shared machine, and the absolute figures drift by up to 25% between runs. What doesn't drift is which of the two won each iteration, so that's counted separately:

WorkloadIterationsprisma-postgrejs faster inOdds of that by luck
Primary-key lookup10196< 1 in 10²²
10k rows, mixed scalars101101< 1 in 10³⁰
10k rows, int8/numeric/timestamp10198< 1 in 10²⁴
Point read10199< 1 in 10²⁶
Page of 20010192< 1 in 10¹⁷
uuid of 5k rows101101< 1 in 10³⁰
float8 of 5k rows101101< 1 in 10³⁰
float8[] of 5k in one row101101< 1 in 10³⁰
int4[] of 100k in one row6161< 1 in 10¹⁸
bytea of 4MB6161< 1 in 10¹⁸
20 inserts in one transaction10196< 1 in 10²²
32 concurrent count(), pool of 46160< 1 in 10¹⁶
32 concurrent findFirst, pool of 46161< 1 in 10¹⁸
32 concurrent findMany, pool of 46161< 1 in 10¹⁸

That last column is a sign test: two adapters of equal speed would split the iterations evenly, so it gives the probability of a split this lopsided from a fair coin. It says the differences are real, and nothing about their size — that's what the speedup column is for.

Result columns arrive in PostgreSQL's binary format and are decoded per type, where pg asks for text and parses it. On bulk that's the whole difference: a 100k-element int4[] costs 19.561 ms and 13.4 MB here against 64.125 ms and 27.3 MB, because the text path has to materialize the array literal as one string before it can parse it. It's cheaper on the wire too, where the text is longer than the value: the 4MB bytea costs 8.0 MB of network under @prisma/adapter-pg and 4.0 MB here, counted at the socket — 2.0 times.

postgrejs names and caches a statement per connection — 64 by default, least-recently-used closed — so each distinct SQL string is parsed and planned once rather than on every call. Counted from the backend: 4 queries through this adapter leave 4 prepared statements behind, and the same 4 through @prisma/adapter-pg leave 0 — a default rather than a limitation, since the reference adapter only names a statement when given a statementNameGenerator, and without one pg sends it unnamed and the server parses it again every time. That accounts for most of the margin on the ordinary rows.

Prisma's query engine sits on both sides of every call, so it compresses these ratios rather than causing them. Measured again through the SqlDriverAdapter alone, on the same SQL the engine emits, the 4MB bytea is 2.30x and allocates 4.2 MB against 52.0 MB, and the int4[] 4.94x. The table above shows the smaller of the two numbers on purpose, since that is the one a caller gets.

Where Prisma wants PostgreSQL's own text — numeric, the date and time family, and their array forms — postgrejs asks the server for it, as a Bind format code, rather than decoding the value and printing it again. So the string is PostgreSQL's own and nothing on this side has to track the session's DateStyle, IntervalStyle or TimeZone to produce it. The reference adapter reaches the same place by switching pg's parsers off one at a time.

Tested against Prisma's own suite​

prisma-postgrejs is run against the functional suite from the prisma/prisma repository at the version it targets — the same suite Prisma runs its own adapters through — with @prisma/adapter-pg over the same server in the same invocation as the control. Tag 7.10.0, PostgreSQL 18.4, 191 suite files selected for provider=postgresql:

@prisma/adapter-pgprisma-postgrejs
Tests12511251
Passed12511251

Both run the same tests and both pass every one of them, so there is no test prisma-postgrejs loses that @prisma/adapter-pg wins. There's no expected-failure list either: the control run measures the baseline on the machine it's run on, so that's the only thing the comparison can fail on. Left out of the table: 81 more that fail identically on both sides and so judge neither adapter — a local clone of the Prisma repository doesn't have the /client/… path its inline snapshots were recorded against.

The package also has 360 tests of its own, including a differential suite that runs every case through @prisma/adapter-pg as well and compares the two. Every divergence below was found by running it.

What changes when you switch​

Measured against @prisma/adapter-pg@7.10.0 on the same schema and the same server.

Values​

Case@prisma/adapter-pgprisma-postgrejs
timetz on any offset but zerowrong instant, silentlycorrect — see below
money below zero in a $queryRaw, e.g. -$0.05throws DecimalError-0.05
money at or above 1000 in a $queryRawthrows DecimalError1234.5
Any temporal type on a server whose DateStyle isn't ISOthrows Invalid time valuecorrect
timestamptz on a session whose TimeZone isn't UTCwrong instant, silentlycorrect
"char"[]the raw literal "{a}"["a"]

The money rows say $queryRaw deliberately: on a model read the query compiler emits "m"::numeric, so the column arrives as a numeric and neither adapter's money handling is reached.

Everywhere else the two agree — including the cases where the raw values differ but the engine converts them to the same thing, checked through a real PrismaClient rather than at the adapter boundary. Where Prisma can't carry a type at all they agree exactly: 23 of 74 PostgreSQL types raise UnsupportedNativeDataType in both.

timetz​

A timetz carries an offset and that offset is the column's data. Prisma appends a Z to whatever it's handed for a Time, so the value has to arrive already at UTC with no offset on it. @prisma/adapter-pg drops the offset and keeps the wall clock; prisma-postgrejs moves the clock to UTC first. On a DateTime @db.Timetz field holding 10:20:30+03 — the instant 07:20:30Z:

Model read$queryRaw
@prisma/adapter-pg10:20:30Z — out by the offset10:20:30Z — out by the offset
prisma-postgrejs07:20:30Z07:20:30Z

Only offset zero agrees. Everywhere else the reference is out by it, and 10:20:30+03 and 10:20:30-05 arrive there as the same value.

Several statements in one $executeRaw​

Both run them; the row count differs:

// t holds 1, 2, 3
await prisma.$executeRawUnsafe(`update t set a = a + 1; delete from t where a = 2`);
// @prisma/adapter-pg -> 0
// prisma-postgrejs -> 4 (3 rows updated, then 1 deleted)

The count is what the contract asks for — "the number of affected rows" — summed over the script. The reference answers 0 for any script, which is a consequence rather than a decision: pg returns an array of results for a multi-command query, and an array has no rowCount.

Inside an interactive transaction it works the same way, on that transaction's own connection — so a rollback takes the script with it.

Errors​

The two produce the same PrismaClientKnownRequestError for the same failure — P2002 for a unique violation, P2010 with the same kind and SQLSTATE for a raw query. Getting there took one thing @prisma/adapter-pg doesn't have to do: postgrejs appends a source excerpt to Error.message wherever the server reported a position, so the mapping reads DatabaseError.serverMessage — the text exactly as PostgreSQL sent it — and a port that missed that would lose the column name from every ColumnNotFound.

Transactions​

Both report usePhantomQuery: false and drive Prisma's savepoints, so nested interactive transactions, all four isolation levels and the SNAPSHOT rejection behave identically, and COMMIT and ROLLBACK appear in PrismaClient's query event and tracing spans exactly as they do with @prisma/adapter-pg. (Neither adapter's BEGIN appears there: both send it themselves, and the engine logs only the statements it issues.)

The difference is the BEGIN: PostgreSQL accepts BEGIN ISOLATION LEVEL SERIALIZABLE as a single statement, so an isolated transaction opens in one round trip where the reference sends BEGIN and then SET TRANSACTION ISOLATION LEVEL.

executeRaw on a SELECT​

pg reports the number of rows a SELECT returned as its rowCount and @prisma/adapter-pg passes that through; postgrejs reports rowsAffected only for INSERT/UPDATE/DELETE/MERGE. This adapter falls back to the row count so the number $executeRaw gives back doesn't change when you switch.

Full documentation​

See the prisma-postgrejs README and its full benchmark report for the complete method and every figure behind the tables above, and prisma.io for Prisma itself — none of that is postgrejs-specific.