Skip to content

Latest commit

Β 

History

808 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

PostgreJS Logo

NPM Version NPM Downloads CI Tests Test Coverage

Why PostgreJS?

PostgreJS is a PostgreSQL driver for Node.js and Bun built from the wire protocol up - no libpq, no native bindings, just TypeScript talking directly to PostgreSQL. That from-scratch design is also what makes it fast and light: every byte on the wire is handled by code written for exactly that purpose, with a binary-first protocol, shared buffers, and row/column decoding pipelines built to avoid unnecessary allocation, instead of generic string plumbing bolted onto a client meant for text.

⚑ Blazing Fast

The numbers back it up. In PostgreJS's own benchmark suite - run head-to-head against pg (node-postgres) and postgres (postgres.js) on identical workloads - PostgreJS opens a connection up to 3x faster than postgres.js, pushes pooled queries through up to 6.5x faster than pg, and fetches large result sets nearly 5x faster than pg too. It's also the only one of the three Node drivers with a complete binary wire protocol across every data type, rather than falling back to text for most of them. See doc/BENCHMARKS.md for the full methodology and every scenario.

The same suite runs under Bun, where Bun's own built-in Bun.sql joins the comparison as a fourth client - see doc/BENCHMARKS-bun.md.

πŸͺΆ Small Footprint

The same benchmarks show it using a fraction of the memory: peak heap usage typically runs 3-7x lower than both pg and postgres.js - as much as 7x lower when streaming cursors or fetching large arrays - and it spends a fraction of the time either of them does in garbage collection. Shared buffers and decode paths that read values straight out of the wire buffer leave far less garbage behind per row, so there's less for the GC to clean up in the first place.

πŸ”‹ Batteries Included

Speed and memory aside, PostgreJS is also the most complete driver of the three: a dynamic sql tag for composable, parameterized SQL, per-query type mapping, and TC39 Explicit Resource Management (using) support are unique to it, alongside a feature set most drivers spread across several add-on packages - connection pooling, prepared statements, server-side cursors, LISTEN/NOTIFY, bulk COPY streams, logical replication, large objects, two-phase commit, multi-host failover, and SCRAM channel binding - all in the one package, written in strictly-typed TypeScript from the ground up.

πŸ”Œ Bring Your Own ORM

PostgreJS is a driver, not a framework, so what you build on top of it stays your choice:

SQB

SQB
@sqb/postgres
native driver
Kysely

Kysely
kysely-postgrejs
dialect
Drizzle ORM

Drizzle ORM
drizzle-postgrejs
driver
TypeORM

TypeORM
typeorm-postgrejs
driver
Prisma

Prisma
prisma-postgrejs
driver adapter

Each adapter hands the compiled SQL straight to this client, with no pg left anywhere in the chain, and each is compared query by query against the driver it replaces - and, where the project ships a suite of its own, run against that too. So adopting one is a change to how the connection is created and nothing else.

Installation

$ npm install postgrejs --save

Documentation

Please read πŸ”Έ DOCUMENTATION πŸ”Έ for detailed usage

Upgrading from 3.5? See doc/MIGRATION-v3.5-to-v3.6.md - query() no longer stops at 100 rows, and fetchAsString now returns the server's own text.

Library Overview

  • Language: Pure JavaScript, with no native/binary dependencies to compile or ship.
  • Strictly typed: Written entirely in TypeScript, with types shipped alongside the package.
  • Modern module format: Ships as ESM; Node 20.19+/22.12+ can require() it from CommonJS code as well.
  • Promise-based API: Every asynchronous operation returns a promise - no callbacks to wrangle.
  • Rigorously tested: A test suite covering the wire protocol, every data type, and connection-handling edge case, run on every push against PostgreSQL 12 through 18.

Feature Comparison

How PostgreJS compares to pg (node-postgres) and postgres (postgres.js). Every row was checked against the libraries' own source rather than their documentation β€” versions compared: PostgreJS 3.4.0, pg 8.23.0, postgres.js 3.4.9. βœ… built in Β· 🟑 partial or needs a separate package Β· ❌ not supported.

Feature PostgreJS pg postgres.js
Packaging
Packages to install 1 4 1 1
Module system ESM ESM/CJS ESM/CJS
Language TS JS 2 JS 3
Wire protocol
Protocol version 3.2 3.0 3.0
Simple Query protocol βœ… βœ… βœ…
Extended Query protocol βœ… βœ… βœ…
Text wire format βœ… βœ… βœ…
Binary wire format βœ… 🟑 4 ❌ 5
Per-column format selection βœ… ❌ ❌
Long cancel key (opt-in) βœ… ❌ ❌
Legacy Function Call protocol βœ… ❌ ❌
Graceful protocol renegotiation βœ… ❌ ❌
High-level API
Object and array row modes βœ… βœ… βœ…
Dynamic SQL helpers βœ… sql tag ❌ βœ…
Per-query type mapping βœ… ❌ 6 ❌ 6
Query cancellation βœ… AbortSignal βœ… βœ…
Per-query timeout βœ… AbortSignal βœ… ❌ 7
Reference counters Connection / Statement ❌ ❌
Caller kept in async error stacks βœ… 🟑 8 🟑 9
Error located in the SQL text βœ… line and mark 🟑 offset 🟑 offset
TC39 Explicit Resource Management βœ… ❌ ❌
Querying
Query parameters βœ… βœ… βœ…
Parameter type casting βœ… 🟑 10 βœ…
Prepared statements 24 βœ… automatic βœ… manual βœ… automatic
Statement-level rollback 25 βœ… automatic ❌ 🟑 manual
Scoped transaction helper 26 βœ… nests ❌ βœ…
Batch execution 21 βœ… ❌ ❌
Multi-statement round trip 23 βœ… ❌ ❌
Multi-statement scripts βœ… βœ… βœ…
Server-side cursors βœ… 🟑 11 βœ…
COPY TO / COPY FROM βœ… 🟑 12 βœ…
Binary COPY encoding 22 βœ… ❌ ❌
Row count after a COPY βœ… βœ… ❌
Transaction management
Transaction API βœ… ❌ βœ…
Savepoints βœ… ❌ βœ…
Two-phase commit API βœ… ❌ 🟑 13
Session management
Built-in connection pool βœ… βœ… βœ… implicit
Pipelining on one connection βœ… opt-in/call βœ… opt-in/client βœ… automatic
Graceful shutdown βœ… ❌ 14 βœ…
Multiple hosts βœ… ❌ βœ…
LISTEN/NOTIFY βœ… 🟑 15 βœ…
Data types
Text encoders 125 generic 16 14
Text decoders 125 44 12 17
Binary encoders 121 27 ❌ ❌
Binary decoders 125 16 ❌
Multidimensional arrays βœ… binary 🟑 text 18 🟑 text
Security
SSL/TLS βœ… βœ… βœ…
Direct TLS negotiation (PG17) βœ… βœ… βœ…
Cleartext, MD5, SCRAM-SHA-256 βœ… βœ… βœ…
SCRAM channel binding (-PLUS) βœ… default βœ… opt-in ❌
Beyond querying
Logical replication βœ… 🟑 19 βœ…
Large object API βœ… ❌ βœ…
Native libpq bindings ❌ 🟑 20 ❌
  • 1 What it takes to reach the feature set above. PostgreJS and postgres.js ship everything in the one package you import; pg needs pg-cursor for cursors, pg-query-stream for row streams and pg-copy-streams for COPY, each installed and versioned separately.
  • 2 Types come from the separate @types/pg; only the pg-protocol and pg-connection-string sub-packages are written in TypeScript.
  • 3 Ships a hand-maintained .d.ts.
  • 4 Results only - parameters are always stringified. Opt-in per query or per client, and all columns at once. No binary parser is registered for bytea, and binary arrays decode only int4, int8 and text elements.
  • 5 Both format-code counts are hardcoded to zero and parameters are stringified, so everything on the wire is text.
  • 6 Global or per-client (pg) and per-instance (postgres.js), but not per query.
  • 7 Connection-level timeouts only.
  • 8 Restored by calling Error.captureStackTrace from the promise's own rejection handler, where the synchronous stack is already gone - the caller's frames come from Node's async stack traces, so they survive an awaited chain but not a .then()/.catch() one, and the callback API gets none at all.
  • 9 Captured at the tagged template, four frames deep; sql.unsafe() gets none.
  • 10 A types array on the query config does reach the Parse message, but the same field doubles as the result parser override, so any row-returning query throws inside pg's own handler. Verified usable only for statements that return no rows (pg 8.23.0).
  • 11 Core has the row-limit primitive; the cursor and stream APIs are separate packages.
  • 12 The core Query refuses COPY IN; pg-copy-streams is required.
  • 13 sql.prepare(name) runs PREPARE TRANSACTION inside begin, but there is no helper for the other half - COMMIT PREPARED / ROLLBACK PREPARED have to be written as raw SQL.
  • 14 end() destroys the socket when a query is still in flight, so the query is aborted rather than awaited; only a client in pipeline mode waits for drain first.
  • 15 On the client only - the pool does not forward notifications.
  • 16 pg has no per-OID text encoders: a parameter is converted by its JavaScript type rather than by the type it is going into, so there is no count to give.
  • 17 Plus every array type, whose OIDs are read from the catalog when a connection opens rather than registered ahead of time.
  • 18 Its binary array decoder covers only int4, int8 and text elements, so everything else falls back to text anyway.
  • 19 A connection flag exists, but nothing decodes the stream.
  • 20 pg-native swaps the pure JavaScript protocol for libpq, and its own documentation lists what stops working with it: pg-cursor, pg-query-stream and pg-copy-streams all "operate directly on the binary stream and therefore are incompatible" - so server-side cursors, row streaming and COPY are what it costs.
  • 21 One statement executed over many parameter sets behind a single Sync, each set's row count reported separately. pg and postgres.js both emit a Sync per execution (syncBuffer in pg's connection.js, the concatenated ExecuteUnnamed in postgres.js's), so a burst of executions costs a server round of implicit-transaction bookkeeping each, pipelined or not. Writing one multi-row statement by hand is a separate approach that all three support, and both PostgreJS and postgres.js ship value builders for it - it is faster still where it applies, but it is one statement rather than many, and PostgreSQL's 65535-parameter ceiling bounds it.
  • 22 Turning JavaScript rows into the binary COPY format, rather than carrying a payload the caller formatted first. All three can carry a COPY stream, but only as bytes: postgres.js hands back a Writable that wraps raw chunks in CopyData, and pg reaches the same point through pg-copy-streams. Producing the format needs a binary encoder per type, which neither driver has - see Binary encoders above, where the same gap shows up for query parameters. The capability is still reachable with pg through a third package, pg-copy-streams-binary, which brings its own encoders; postgres.js has no equivalent.
  • 23 Several different statements sent under one Sync, so they cost a single round of the server's transaction bookkeeping rather than one each, arrive as one implicit transaction, and come back as per-statement results. Distinct from the Pipelining row above, which is about not waiting between queries: all three do that, and all three still close every statement with its own Sync - pg sends one immediately after each Execute (query.js), and postgres.js concatenates Execute and Sync into a single constant (ExecuteUnnamed). Atomicity on its own is reachable anywhere with an explicit BEGIN/COMMIT; what that cannot recover is the round trip, since the per-statement Syncs and two extra statements remain.
  • 24 Whether a repeated query is parsed again every time. PostgreJS and postgres.js both keep a per-connection cache keyed on the SQL and reuse a server-side statement, so a repeat costs Bind/Execute rather than Parse/Bind/Describe/Execute; pg prepares only the statements you name yourself (query.js: "named queries must always be prepared"), with nothing caching by SQL text. The two automatic ones differ in when they start: postgres.js on first sight, PostgreJS on the second use, which leaves a genuinely one-shot query at its unprepared cost. Both default to on and both take prepare: false, which matters for PgBouncer in transaction pooling mode before 1.21, where a named statement does not survive to the next call.
  • 25 Whether one failed statement inside a transaction leaves the rest of the block runnable. PostgreJS puts every statement under a savepoint of its own by default (rollbackOnError), rolling back to it when the statement fails, and carries the SAVEPOINT/RELEASE pair inside the statement's own round trip rather than paying a round trip for each. postgres.js has savepoints, but as a scope the caller opens around a callback (savepoint(name, fn) in src/index.js), so a statement is protected only where someone wrapped it; pg has no savepoint handling in lib/ at all, so a failed statement leaves the block aborted until the caller rolls back themselves.
  • 26 Whether the library runs a callback inside a transaction for you, committing or rolling back on the way out. PostgreJS has transaction(fn) on both a connection and the pool, and a call made inside one already open takes a savepoint, so nesting needs nothing from the caller. postgres.js has sql.begin(fn) (src/index.js), where a nested scope is opened explicitly with sql.savepoint(fn) instead. pg has no such helper in lib/ - BEGIN and COMMIT are statements the caller sends, and getting a transaction onto one connection is the caller's problem too.
  • 27 Every registered type but tsvector and tsquery, which have none deliberately: the server's own input parser is what sorts and deduplicates a vector and what defines a query's grammar, so encoding either here would make the same text mean one thing written as a literal and another bound as a parameter. They are sent as text instead, which is exact.

Benchmarks

PostgreJS implements the full PostgreSQL wire protocol from scratch, with no dependency on pg/libpq. doc/BENCHMARKS.md compares it against pg (node-postgres) and postgres (postgres.js) across connection setup, simple/prepared queries, mixed-type decoding, bulk fetches, cursor streaming and pool concurrency, each library run through its own idiomatic fast path. The numbers there are reproducible on your own machine via npm run bench against the repo's own docker/docker-compose.yml Postgres instance; see benchmark/README.md for details.

The same suite runs under Bun via npm run bench:bun, where it also measures Bun's own built-in Bun.sql client as a fourth library. Those results live in doc/BENCHMARKS-bun.md and are deliberately kept in a separate report rather than merged into the Node one: GC instrumentation, cursor streaming and binary-format control all differ enough between the two runtimes that a single table would conflate a library difference with a runtime difference.

Both reports list libraries in a fixed order rather than fastest-first, and mark every result statistically tied with the leader rather than bolding a lone winner - several scenarios separate the leading drivers by about a percent, which is less than the run-to-run spread of the measurement itself.

Support

You can report bugs and discuss features on the GitHub issues page When you open an issue please provide version of NodeJS and PostgreSQL server.

Runtime Compatibility

  • Node.js >= 20.x
  • Bun - the same test suite runs under Bun in CI against PostgreSQL 18 on every push, so Bun is a supported target rather than an untested coincidence. (Coverage is collected on the Node matrix only: c8 instruments V8's coverage APIs, which JavaScriptCore doesn't have.)

License

PostgreJS is available under the BSD 3-Clause license.

About

Professional PostgreSQL client for NodeJS and Bun

Topics

Resources

Code of conduct

Contributing

Stars

71 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages