Compare commits
27 Commits
eeed8b2f66
...
v0.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
7f3e3b236b
|
|||
|
3d65dcecf2
|
|||
|
90dc51a914
|
|||
|
a1b66c4c48
|
|||
|
119c06565c
|
|||
|
d959a80678
|
|||
|
c2ff6b4359
|
|||
|
a4c0055c79
|
|||
|
00002525e4
|
|||
|
328cc63536
|
|||
|
29b79f25c0
|
|||
|
29b2796627
|
|||
|
02f8098811
|
|||
|
da7f7e12f3
|
|||
|
6f9e9770cf
|
|||
|
137422601b
|
|||
|
3793e14f50
|
|||
|
b194397645
|
|||
|
858b7a95f3
|
|||
|
a88da00dec
|
|||
|
cefe14b9dc
|
|||
|
4e68e34fd0
|
|||
|
826190ecc9
|
|||
|
72749e5841
|
|||
|
b9829bc70d
|
|||
|
9eecf29bc5
|
|||
|
8964cb342e
|
121
README.md
121
README.md
@@ -1,14 +1,35 @@
|
||||
# pglue
|
||||
|
||||
## Performance
|
||||
The glue for TypeScript to PostgreSQL.
|
||||
|
||||
pglue implements automatic query pipelining which makes it especially performant with many queries concurrently executed on a single connection.
|
||||
## Overview
|
||||
|
||||
- 🌟 [High performance](#benchmarks), fully asynchronous, written in modern TypeScript
|
||||
- 🐢 First class Deno support
|
||||
- 💬 Automatic query parameterisation
|
||||
- 🌧️ Automatic query pipelining
|
||||
- 📣 Listen/notify support
|
||||
- 📤 Connection pool support
|
||||
|
||||
## Installation
|
||||
|
||||
```ts
|
||||
import pglue from "https://git.lua.re/luaneko/pglue/raw/tag/v0.3.2/mod.ts";
|
||||
// ...or from github:
|
||||
import pglue from "https://raw.githubusercontent.com/luaneko/pglue/refs/tags/v0.3.2/mod.ts";
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
TODO: Write the documentation in more detail here.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Performance is generally on par with [postgres.js][1] and up to **4x faster** than [deno-postgres][2]. Keep in mind that database driver benchmarks are largely dependent on the database performance itself and does not necessarily represent accurate real-world performance.
|
||||
Performance is generally on par with [postgres.js][1] and up to **5x faster** than [deno-postgres][2]. Keep in mind that database driver benchmarks are largely dependent on the database performance itself and does not necessarily represent accurate real-world performance.
|
||||
|
||||
Tested on a 4 core 2800 MHz x86_64-pc-linux-gnu QEMU VM with Deno 2.1.4 and local PostgreSQL 17.1 installation connected via TCP on localhost:
|
||||
Tested on a 4 core, 2800 MHz, x86_64-pc-linux-gnu, QEMU VM, with Deno 2.1.4 and PostgreSQL 17.1 on localhost:
|
||||
|
||||
Query `select * from pg_type`:
|
||||
|
||||
```
|
||||
CPU | Common KVM Processor v2.0
|
||||
@@ -18,74 +39,78 @@ benchmark time/iter (avg) iter/s (min … max) p75
|
||||
--------------- ----------------------------- --------------------- --------------------------
|
||||
|
||||
group select n=1
|
||||
pglue 9.9 ms 101.1 ( 7.9 ms … 17.8 ms) 10.2 ms 17.8 ms 17.8 ms
|
||||
postgres.js 8.8 ms 114.2 ( 7.0 ms … 9.5 ms) 9.1 ms 9.5 ms 9.5 ms
|
||||
deno-postgres 37.4 ms 26.7 ( 25.3 ms … 42.8 ms) 39.2 ms 42.8 ms 42.8 ms
|
||||
pglue 8.8 ms 113.8 ( 7.2 ms … 11.8 ms) 9.7 ms 11.8 ms 11.8 ms
|
||||
postgres.js 10.8 ms 92.3 ( 8.1 ms … 22.0 ms) 11.2 ms 22.0 ms 22.0 ms
|
||||
deno-postgres 38.9 ms 25.7 ( 23.5 ms … 51.9 ms) 40.3 ms 51.9 ms 51.9 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.13x slower than postgres.js
|
||||
3.78x faster than deno-postgres
|
||||
1.23x faster than postgres.js
|
||||
4.42x faster than deno-postgres
|
||||
|
||||
group select n=5
|
||||
pglue 48.2 ms 20.8 ( 41.9 ms … 68.5 ms) 50.3 ms 68.5 ms 68.5 ms
|
||||
postgres.js 43.6 ms 22.9 ( 38.1 ms … 57.3 ms) 48.6 ms 57.3 ms 57.3 ms
|
||||
deno-postgres 186.5 ms 5.4 (138.4 ms … 213.2 ms) 193.6 ms 213.2 ms 213.2 ms
|
||||
pglue 40.1 ms 25.0 ( 36.1 ms … 48.2 ms) 40.7 ms 48.2 ms 48.2 ms
|
||||
postgres.js 48.7 ms 20.5 ( 38.9 ms … 61.2 ms) 52.7 ms 61.2 ms 61.2 ms
|
||||
deno-postgres 184.7 ms 5.4 (166.5 ms … 209.5 ms) 190.7 ms 209.5 ms 209.5 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.11x slower than postgres.js
|
||||
3.87x faster than deno-postgres
|
||||
1.22x faster than postgres.js
|
||||
4.61x faster than deno-postgres
|
||||
|
||||
group select n=10
|
||||
pglue 97.8 ms 10.2 ( 90.2 ms … 105.0 ms) 104.0 ms 105.0 ms 105.0 ms
|
||||
postgres.js 93.8 ms 10.7 ( 80.9 ms … 107.7 ms) 106.1 ms 107.7 ms 107.7 ms
|
||||
deno-postgres 333.9 ms 3.0 (205.6 ms … 394.9 ms) 377.4 ms 394.9 ms 394.9 ms
|
||||
pglue 80.7 ms 12.4 ( 73.5 ms … 95.4 ms) 82.2 ms 95.4 ms 95.4 ms
|
||||
postgres.js 89.1 ms 11.2 ( 82.5 ms … 101.7 ms) 94.4 ms 101.7 ms 101.7 ms
|
||||
deno-postgres 375.3 ms 2.7 (327.4 ms … 393.9 ms) 390.7 ms 393.9 ms 393.9 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.10x faster than postgres.js
|
||||
4.65x faster than deno-postgres
|
||||
```
|
||||
|
||||
Query `insert into my_table (a, b, c) values (${a}, ${b}, ${c})`:
|
||||
|
||||
```
|
||||
group insert n=1
|
||||
pglue 259.2 µs 3,858 (165.4 µs … 2.8 ms) 258.0 µs 775.4 µs 2.8 ms
|
||||
postgres.js 235.9 µs 4,239 (148.8 µs … 1.2 ms) 250.3 µs 577.4 µs 585.6 µs
|
||||
deno-postgres 306.7 µs 3,260 (198.8 µs … 1.3 ms) 325.9 µs 1.0 ms 1.3 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.10x slower than postgres.js
|
||||
1.18x faster than deno-postgres
|
||||
|
||||
group insert n=10
|
||||
pglue 789.7 µs 1,266 (553.2 µs … 2.7 ms) 783.4 µs 2.4 ms 2.7 ms
|
||||
postgres.js 755.6 µs 1,323 (500.5 µs … 3.4 ms) 795.0 µs 2.8 ms 3.4 ms
|
||||
deno-postgres 2.2 ms 458.1 ( 1.6 ms … 5.2 ms) 2.3 ms 4.8 ms 5.2 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.04x slower than postgres.js
|
||||
3.42x faster than deno-postgres
|
||||
|
||||
group insert n=1
|
||||
pglue 237.5 µs 4,210 (143.9 µs … 1.3 ms) 249.2 µs 953.3 µs 1.3 ms
|
||||
postgres.js 242.5 µs 4,124 (137.4 µs … 886.4 µs) 263.4 µs 762.8 µs 865.5 µs
|
||||
deno-postgres 295.1 µs 3,389 (163.8 µs … 899.3 µs) 340.0 µs 641.7 µs 899.3 µs
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.02x faster than postgres.js
|
||||
1.24x faster than deno-postgres
|
||||
|
||||
group insert n=10
|
||||
pglue 1.1 ms 869.6 (610.1 µs … 2.1 ms) 1.2 ms 2.0 ms 2.1 ms
|
||||
postgres.js 755.9 µs 1,323 (387.6 µs … 4.7 ms) 805.4 µs 2.8 ms 4.7 ms
|
||||
deno-postgres 2.3 ms 434.4 ( 1.6 ms … 10.6 ms) 2.4 ms 6.5 ms 10.6 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.52x slower than postgres.js
|
||||
2.00x faster than deno-postgres
|
||||
2.76x faster than deno-postgres
|
||||
|
||||
group insert n=100
|
||||
pglue 9.2 ms 109.0 ( 5.5 ms … 15.6 ms) 10.4 ms 15.6 ms 15.6 ms
|
||||
postgres.js 14.8 ms 67.4 ( 9.6 ms … 35.8 ms) 16.6 ms 35.8 ms 35.8 ms
|
||||
deno-postgres 18.8 ms 53.1 ( 14.5 ms … 25.8 ms) 20.9 ms 25.8 ms 25.8 ms
|
||||
pglue 5.8 ms 172.0 ( 3.2 ms … 9.9 ms) 6.8 ms 9.9 ms 9.9 ms
|
||||
postgres.js 13.0 ms 76.8 ( 8.6 ms … 20.8 ms) 15.4 ms 20.8 ms 20.8 ms
|
||||
deno-postgres 18.5 ms 54.1 ( 14.3 ms … 32.1 ms) 20.0 ms 32.1 ms 32.1 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.62x faster than postgres.js
|
||||
2.05x faster than deno-postgres
|
||||
2.24x faster than postgres.js
|
||||
3.18x faster than deno-postgres
|
||||
|
||||
group insert n=200
|
||||
pglue 15.0 ms 66.6 ( 11.1 ms … 19.0 ms) 16.7 ms 19.0 ms 19.0 ms
|
||||
postgres.js 28.1 ms 35.6 ( 22.8 ms … 40.0 ms) 29.1 ms 40.0 ms 40.0 ms
|
||||
deno-postgres 35.9 ms 27.9 ( 29.7 ms … 46.5 ms) 37.2 ms 46.5 ms 46.5 ms
|
||||
pglue 8.8 ms 113.4 ( 6.0 ms … 14.1 ms) 10.0 ms 14.1 ms 14.1 ms
|
||||
postgres.js 28.2 ms 35.5 ( 21.1 ms … 47.0 ms) 29.6 ms 47.0 ms 47.0 ms
|
||||
deno-postgres 37.0 ms 27.0 ( 32.0 ms … 48.1 ms) 39.4 ms 48.1 ms 48.1 ms
|
||||
|
||||
summary
|
||||
pglue
|
||||
1.87x faster than postgres.js
|
||||
2.39x faster than deno-postgres
|
||||
3.20x faster than postgres.js
|
||||
4.20x faster than deno-postgres
|
||||
```
|
||||
|
||||
[1]: https://github.com/porsager/postgres
|
||||
|
||||
2
bench.ts
2
bench.ts
@@ -1,4 +1,4 @@
|
||||
import * as pglue from "./mod.ts";
|
||||
import pglue from "./mod.ts";
|
||||
import postgres_js from "https://deno.land/x/postgresjs/mod.js";
|
||||
import * as deno_postgres from "https://deno.land/x/postgres/mod.ts";
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"name": "@luaneko/pglue",
|
||||
"version": "0.1.0",
|
||||
"exports": "./mod.ts",
|
||||
"tasks": {
|
||||
"test": "deno run --watch -A mod_test.ts",
|
||||
"bench": "deno bench --watch -A"
|
||||
}
|
||||
"version": "0.3.2",
|
||||
"exports": "./mod.ts"
|
||||
}
|
||||
|
||||
45
deno.lock
generated
45
deno.lock
generated
@@ -2,23 +2,50 @@
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@badrap/valita@~0.4.2": "0.4.2",
|
||||
"jsr:@std/assert@^1.0.10": "1.0.10",
|
||||
"jsr:@std/bytes@^1.0.3": "1.0.4",
|
||||
"jsr:@std/bytes@^1.0.4": "1.0.4",
|
||||
"jsr:@std/encoding@^1.0.6": "1.0.6",
|
||||
"jsr:@std/expect@*": "1.0.11",
|
||||
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||
"jsr:@std/streams@*": "1.0.8",
|
||||
"npm:pg-connection-string@^2.7.0": "2.7.0"
|
||||
},
|
||||
"jsr": {
|
||||
"@badrap/valita@0.4.2": {
|
||||
"integrity": "af8a829e82eac71adbc7b60352798f94dcc66d19fab16b657957ca9e646c25fd"
|
||||
},
|
||||
"@std/assert@1.0.10": {
|
||||
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/bytes@1.0.4": {
|
||||
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
|
||||
},
|
||||
"@std/encoding@1.0.6": {
|
||||
"integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069"
|
||||
},
|
||||
"@std/expect@1.0.11": {
|
||||
"integrity": "5aa5d5cf891e9a3249e45ea770de15189e5a2faee2122ee5746b10d1c310a19b",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert",
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.5": {
|
||||
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||
},
|
||||
"@std/path@1.0.8": {
|
||||
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||
},
|
||||
"@std/streams@1.0.8": {
|
||||
"integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3",
|
||||
"dependencies": [
|
||||
"jsr:@std/bytes@^1.0.3"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
@@ -433,11 +460,17 @@
|
||||
"https://deno.land/x/postgresjs@v3.4.5/src/result.js": "001ff5e0c8d634674f483d07fbcd620a797e3101f842d6c20ca3ace936260465",
|
||||
"https://deno.land/x/postgresjs@v3.4.5/src/subscribe.js": "9e4d0c3e573a6048e77ee2f15abbd5bcd17da9ca85a78c914553472c6d6c169b",
|
||||
"https://deno.land/x/postgresjs@v3.4.5/src/types.js": "471f4a6c35412aa202a7c177c0a7e5a7c3bd225f01bbde67c947894c1b8bf6ed",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/async.ts": "20bc54c7260c2d2cd27ffcca33b903dde57a3a3635386d8e0c6baca4b253ae4e",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/bytes.ts": "5ffb12787dc3f9ef9680b6e2e4f5f9903783aa4c33b69e725b5df1d1c116bfe6",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/events.ts": "28d395b8eea87f9bf7908a44b351d2d3c609ba7eab62bcecd0d43be8ee603438",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/func.ts": "f1935f673365cd68939531d65ef18fe81b5d43dc795b03c34bb5ad821ab1c9ff",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/jit.ts": "c1db7820de95c48521b057c7cdf9aa41f7eaba77462407c29d3932e7da252d53",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/mod.ts": "95d8b15048a54cb82391825831f695b74e7c8b206317264a99c906ce25c63f13"
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.0/async.ts": "20bc54c7260c2d2cd27ffcca33b903dde57a3a3635386d8e0c6baca4b253ae4e",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.0/bytes.ts": "5ffb12787dc3f9ef9680b6e2e4f5f9903783aa4c33b69e725b5df1d1c116bfe6",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.0/events.ts": "28d395b8eea87f9bf7908a44b351d2d3c609ba7eab62bcecd0d43be8ee603438",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.0/func.ts": "f1935f673365cd68939531d65ef18fe81b5d43dc795b03c34bb5ad821ab1c9ff",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.0/jit.ts": "c1db7820de95c48521b057c7cdf9aa41f7eaba77462407c29d3932e7da252d53",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.0/mod.ts": "95d8b15048a54cb82391825831f695b74e7c8b206317264a99c906ce25c63f13",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/async.ts": "20bc54c7260c2d2cd27ffcca33b903dde57a3a3635386d8e0c6baca4b253ae4e",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/bytes.ts": "94f4809b375800bb2c949e31082dfdf08d022db56c5b5c9c7dfe6f399285da6f",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/events.ts": "28d395b8eea87f9bf7908a44b351d2d3c609ba7eab62bcecd0d43be8ee603438",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/func.ts": "f1935f673365cd68939531d65ef18fe81b5d43dc795b03c34bb5ad821ab1c9ff",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/jit.ts": "c1db7820de95c48521b057c7cdf9aa41f7eaba77462407c29d3932e7da252d53",
|
||||
"https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/mod.ts": "589763be8ab18e7d6c5f5921e74ab44580f466c92acead401b2903d42d94112a"
|
||||
}
|
||||
}
|
||||
|
||||
2
lstd.ts
2
lstd.ts
@@ -1 +1 @@
|
||||
export * from "https://git.lua.re/luaneko/lstd/raw/tag/0.2.0/mod.ts";
|
||||
export * from "https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/mod.ts";
|
||||
|
||||
125
mod.ts
125
mod.ts
@@ -1,23 +1,22 @@
|
||||
import pg_conn_string from "npm:pg-connection-string@^2.7.0";
|
||||
import {
|
||||
type Infer,
|
||||
number,
|
||||
object,
|
||||
record,
|
||||
string,
|
||||
union,
|
||||
unknown,
|
||||
} from "./valita.ts";
|
||||
import { Pool, wire_connect, type LogLevel } from "./wire.ts";
|
||||
import { sql_types, type SqlType, type SqlTypeMap } from "./query.ts";
|
||||
import pg_conn_str from "npm:pg-connection-string@^2.7.0";
|
||||
import type * as v from "./valita.ts";
|
||||
import { Pool, PoolOptions, Wire, WireOptions } from "./wire.ts";
|
||||
|
||||
export {
|
||||
Wire,
|
||||
WireOptions,
|
||||
WireError,
|
||||
PostgresError,
|
||||
Pool,
|
||||
PoolOptions,
|
||||
type WireEvents,
|
||||
type PoolEvents,
|
||||
type LogLevel,
|
||||
type Parameters,
|
||||
type Transaction,
|
||||
type Channel,
|
||||
type Parameters,
|
||||
type ChannelEvents,
|
||||
type NotificationHandler,
|
||||
} from "./wire.ts";
|
||||
export {
|
||||
type SqlFragment,
|
||||
@@ -25,96 +24,66 @@ export {
|
||||
type SqlTypeMap,
|
||||
SqlTypeError,
|
||||
sql,
|
||||
sql_types,
|
||||
sql_format,
|
||||
is_sql,
|
||||
Query,
|
||||
type Row,
|
||||
type CommandResult,
|
||||
type Result,
|
||||
type Results,
|
||||
type ResultStream,
|
||||
type Row,
|
||||
type Rows,
|
||||
type RowStream,
|
||||
} from "./query.ts";
|
||||
|
||||
export type Options = {
|
||||
host?: string;
|
||||
port?: number | string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
database?: string | null;
|
||||
max_connections?: number;
|
||||
idle_timeout?: number;
|
||||
runtime_params?: Record<string, string>;
|
||||
types?: SqlTypeMap;
|
||||
};
|
||||
export default function postgres(s: string, options: Partial<Options> = {}) {
|
||||
return new Postgres(Options.parse(parse_conn(s, options), { mode: "strip" }));
|
||||
}
|
||||
|
||||
type ParsedOptions = Infer<typeof ParsedOptions>;
|
||||
const ParsedOptions = object({
|
||||
host: string().optional(() => "localhost"),
|
||||
port: union(
|
||||
number(),
|
||||
string().map((s) => parseInt(s, 10))
|
||||
).optional(() => 5432),
|
||||
user: string().optional(() => "postgres"),
|
||||
password: string().optional(() => "postgres"),
|
||||
database: string()
|
||||
.nullable()
|
||||
.optional(() => null),
|
||||
runtime_params: record(string()).optional(() => ({})),
|
||||
max_connections: number().optional(() => 10),
|
||||
idle_timeout: number().optional(() => 20),
|
||||
types: record(unknown())
|
||||
.optional(() => ({}))
|
||||
.map((types): SqlTypeMap => ({ ...sql_types, ...types })),
|
||||
});
|
||||
|
||||
function parse_opts(s: string, opts: Options) {
|
||||
function parse_conn(s: string, options: Partial<WireOptions>) {
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
ssl: _ssl, // TODO:
|
||||
ssl: _ssl, // TODO: ssl support
|
||||
...runtime_params
|
||||
} = pg_conn_string.parse(s);
|
||||
} = s ? pg_conn_str.parse(s) : {};
|
||||
|
||||
const { PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, USER } =
|
||||
Deno.env.toObject();
|
||||
|
||||
return ParsedOptions.parse({
|
||||
...opts,
|
||||
host: opts.host ?? host ?? PGHOST ?? undefined,
|
||||
port: opts.port ?? port ?? PGPORT ?? undefined,
|
||||
user: opts.user ?? user ?? PGUSER ?? USER ?? undefined,
|
||||
password: opts.password ?? password ?? PGPASSWORD ?? undefined,
|
||||
database: opts.database ?? database ?? PGDATABASE ?? undefined,
|
||||
runtime_params: { ...runtime_params, ...opts.runtime_params },
|
||||
});
|
||||
}
|
||||
|
||||
export default function postgres(s: string, options: Options = {}) {
|
||||
return new Postgres(parse_opts(s, options));
|
||||
}
|
||||
|
||||
export function connect(s: string, options: Options = {}) {
|
||||
return wire_connect(parse_opts(s, options));
|
||||
return {
|
||||
...options,
|
||||
host: options.host ?? host,
|
||||
port: options.port ?? port,
|
||||
user: options.user ?? user,
|
||||
password: options.password ?? password,
|
||||
database: options.database ?? database,
|
||||
runtime_params: { ...runtime_params, ...options.runtime_params },
|
||||
};
|
||||
}
|
||||
|
||||
postgres.connect = connect;
|
||||
|
||||
export type PostgresEvents = {
|
||||
log(level: LogLevel, ctx: object, msg: string): void;
|
||||
};
|
||||
export async function connect(s: string, options: Partial<WireOptions> = {}) {
|
||||
return await new Wire(
|
||||
WireOptions.parse(parse_conn(s, options), { mode: "strip" })
|
||||
).connect();
|
||||
}
|
||||
|
||||
export type Options = v.Infer<typeof Options>;
|
||||
export const Options = PoolOptions;
|
||||
|
||||
export class Postgres extends Pool {
|
||||
readonly #options;
|
||||
|
||||
constructor(options: ParsedOptions) {
|
||||
constructor(options: Options) {
|
||||
super(options);
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const wire = await wire_connect(this.#options);
|
||||
return wire.on("log", (l, c, s) => this.emit("log", l, c, s));
|
||||
async connect(options: Partial<WireOptions> = {}) {
|
||||
return await new Wire(
|
||||
WireOptions.parse({ ...this.#options, ...options }, { mode: "strip" })
|
||||
)
|
||||
.on("log", (l, c, s) => this.emit("log", l, c, s))
|
||||
.connect();
|
||||
}
|
||||
}
|
||||
|
||||
23
mod_test.ts
23
mod_test.ts
@@ -1,23 +0,0 @@
|
||||
import postgres from "./mod.ts";
|
||||
|
||||
await using pool = postgres(`postgres://test:test@localhost:5432/test`, {
|
||||
runtime_params: { client_min_messages: "INFO" },
|
||||
});
|
||||
|
||||
pool.on("log", (level, ctx, msg) => console.info(`${level}: ${msg}`, ctx));
|
||||
|
||||
await pool.begin(async (pg, tx) => {
|
||||
await pg.query`
|
||||
create table my_test (
|
||||
key integer primary key generated always as identity,
|
||||
data text not null
|
||||
)
|
||||
`;
|
||||
|
||||
await pg.query`
|
||||
insert into my_test (data) values (${[1, 2, 3]}::bytea)
|
||||
`;
|
||||
|
||||
console.log(await pg.query`select * from my_test`);
|
||||
await tx.rollback();
|
||||
});
|
||||
174
query.ts
174
query.ts
@@ -1,4 +1,4 @@
|
||||
import type { ObjectType } from "./valita.ts";
|
||||
import type * as v from "./valita.ts";
|
||||
import { from_hex, to_hex, to_utf8 } from "./lstd.ts";
|
||||
|
||||
export const sql_format = Symbol.for(`re.lua.pglue.sql_format`);
|
||||
@@ -127,10 +127,36 @@ export const bool: SqlType = {
|
||||
return s !== "f";
|
||||
},
|
||||
output(x) {
|
||||
return typeof x === "undefined" || x === null ? null : x ? "t" : "f";
|
||||
if (typeof x === "undefined" || x === null) return null;
|
||||
const b = bool_names[String(x).toLowerCase()];
|
||||
if (typeof b === "boolean") return b ? "t" : "f";
|
||||
else throw new SqlTypeError(`invalid bool output '${x}'`);
|
||||
},
|
||||
};
|
||||
|
||||
const bool_names: Partial<Record<string, boolean>> = {
|
||||
// https://www.postgresql.org/docs/current/datatype-boolean.html#DATATYPE-BOOLEAN
|
||||
t: true,
|
||||
tr: true,
|
||||
tru: true,
|
||||
true: true,
|
||||
y: true,
|
||||
ye: true,
|
||||
yes: true,
|
||||
on: true,
|
||||
1: true,
|
||||
f: false,
|
||||
fa: false,
|
||||
fal: false,
|
||||
fals: false,
|
||||
false: false,
|
||||
n: false,
|
||||
no: false,
|
||||
of: false,
|
||||
off: false,
|
||||
0: false,
|
||||
};
|
||||
|
||||
export const text: SqlType = {
|
||||
input(s) {
|
||||
return s;
|
||||
@@ -142,6 +168,23 @@ export const text: SqlType = {
|
||||
},
|
||||
};
|
||||
|
||||
export const char: SqlType = {
|
||||
input(c) {
|
||||
const n = c.charCodeAt(0);
|
||||
if (c.length === 1 && 0 <= n && n <= 255) return c;
|
||||
throw new SqlTypeError(`invalid char input '${c}'`);
|
||||
},
|
||||
output(x) {
|
||||
let c: string;
|
||||
if (typeof x === "undefined" || x === null) return null;
|
||||
else if (typeof x === "number") c = String.fromCharCode(x);
|
||||
else c = String(x);
|
||||
const n = c.charCodeAt(0);
|
||||
if (c.length === 1 && 0 <= n && n <= 255) return c;
|
||||
else throw new SqlTypeError(`invalid char output '${x}'`);
|
||||
},
|
||||
};
|
||||
|
||||
export const int2: SqlType = {
|
||||
input(s) {
|
||||
const n = Number(s);
|
||||
@@ -175,6 +218,22 @@ export const int4: SqlType = {
|
||||
},
|
||||
};
|
||||
|
||||
export const uint4: SqlType = {
|
||||
input(s) {
|
||||
const n = Number(s);
|
||||
if (Number.isInteger(n) && 0 <= n && n <= 4294967295) return n;
|
||||
else throw new SqlTypeError(`invalid uint4 input '${s}'`);
|
||||
},
|
||||
output(x) {
|
||||
let n: number;
|
||||
if (typeof x === "undefined" || x === null) return null;
|
||||
else if (typeof x === "number") n = x;
|
||||
else n = Number(x);
|
||||
if (Number.isInteger(n) && 0 <= n && n <= 4294967295) return n.toString();
|
||||
else throw new SqlTypeError(`invalid uint4 output '${x}'`);
|
||||
},
|
||||
};
|
||||
|
||||
export const int8: SqlType = {
|
||||
input(s) {
|
||||
const n = BigInt(s);
|
||||
@@ -188,14 +247,36 @@ export const int8: SqlType = {
|
||||
else if (typeof x === "number" || typeof x === "bigint") n = x;
|
||||
else if (typeof x === "string") n = BigInt(x);
|
||||
else n = Number(x);
|
||||
if (Number.isInteger(n)) {
|
||||
if (-9007199254740991 <= n && n <= 9007199254740991) return n.toString();
|
||||
else throw new SqlTypeError(`unsafe int8 output '${x}'`);
|
||||
} else if (typeof n === "bigint") {
|
||||
if (-9223372036854775808n <= n && n <= 9223372036854775807n)
|
||||
return n.toString();
|
||||
}
|
||||
throw new SqlTypeError(`invalid int8 output '${x}'`);
|
||||
if (
|
||||
(typeof n === "number" && Number.isSafeInteger(n)) ||
|
||||
(typeof n === "bigint" &&
|
||||
-9223372036854775808n <= n &&
|
||||
n <= 9223372036854775807n)
|
||||
) {
|
||||
return n.toString();
|
||||
} else throw new SqlTypeError(`invalid int8 output '${x}'`);
|
||||
},
|
||||
};
|
||||
|
||||
export const uint8: SqlType = {
|
||||
input(s) {
|
||||
const n = BigInt(s);
|
||||
if (0n <= n && n <= 9007199254740991n) return Number(n);
|
||||
else if (0n <= n && n <= 18446744073709551615n) return n;
|
||||
else throw new SqlTypeError(`invalid uint8 input '${s}'`);
|
||||
},
|
||||
output(x) {
|
||||
let n: number | bigint;
|
||||
if (typeof x === "undefined" || x === null) return null;
|
||||
else if (typeof x === "number" || typeof x === "bigint") n = x;
|
||||
else if (typeof x === "string") n = BigInt(x);
|
||||
else n = Number(x);
|
||||
if (
|
||||
(typeof n === "number" && Number.isSafeInteger(n) && 0 <= n) ||
|
||||
(typeof n === "bigint" && 0n <= n && n <= 18446744073709551615n)
|
||||
) {
|
||||
return n.toString();
|
||||
} else throw new SqlTypeError(`invalid uint8 output '${x}'`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -279,60 +360,63 @@ export const json: SqlType = {
|
||||
};
|
||||
|
||||
export const sql_types: SqlTypeMap = {
|
||||
0: text,
|
||||
16: bool, // bool
|
||||
25: text, // text
|
||||
17: bytea, // bytea
|
||||
18: char, // char
|
||||
19: text, // name
|
||||
20: int8, // int8
|
||||
21: int2, // int2
|
||||
23: int4, // int4
|
||||
20: int8, // int8
|
||||
26: int8, // oid
|
||||
25: text, // text
|
||||
26: uint4, // oid
|
||||
28: uint4, // xid
|
||||
29: uint4, // cid
|
||||
114: json, // json
|
||||
700: float4, // float4
|
||||
701: float8, // float8
|
||||
1082: timestamptz, // date
|
||||
1114: timestamptz, // timestamp
|
||||
1184: timestamptz, // timestamptz
|
||||
17: bytea, // bytea
|
||||
114: json, // json
|
||||
3802: json, // jsonb
|
||||
5069: uint8, // xid8
|
||||
};
|
||||
|
||||
sql.types = sql_types;
|
||||
|
||||
type ReadonlyTuple<T extends readonly unknown[]> = readonly [...T];
|
||||
|
||||
export interface CommandResult {
|
||||
export interface Result {
|
||||
readonly tag: string;
|
||||
}
|
||||
|
||||
export interface Result<T> extends CommandResult, ReadonlyTuple<[T]> {
|
||||
readonly row: T;
|
||||
}
|
||||
|
||||
export interface Results<T> extends CommandResult, ReadonlyArray<T> {
|
||||
export interface Rows<T> extends Result, ReadonlyArray<T> {
|
||||
readonly rows: ReadonlyArray<T>;
|
||||
}
|
||||
|
||||
export interface ResultStream<T>
|
||||
extends AsyncIterable<T[], CommandResult, void> {}
|
||||
export interface RowStream<T> extends AsyncIterable<T[], Result, void> {}
|
||||
|
||||
export interface Row extends Iterable<unknown, void, void> {
|
||||
[column: string]: unknown;
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
readonly simple: boolean;
|
||||
readonly chunk_size: number;
|
||||
readonly stdin: ReadableStream<Uint8Array> | null;
|
||||
readonly stdout: WritableStream<Uint8Array> | null;
|
||||
}
|
||||
|
||||
export class Query<T = Row>
|
||||
implements PromiseLike<Results<T>>, ResultStream<T>
|
||||
{
|
||||
export class Query<T = Row> implements PromiseLike<Rows<T>>, RowStream<T> {
|
||||
readonly #f;
|
||||
|
||||
constructor(f: (options: Partial<QueryOptions>) => ResultStream<T>) {
|
||||
constructor(f: (options: Partial<QueryOptions>) => RowStream<T>) {
|
||||
this.#f = f;
|
||||
}
|
||||
|
||||
simple(simple = true) {
|
||||
const f = this.#f;
|
||||
return new Query((o) => f({ simple, ...o }));
|
||||
}
|
||||
|
||||
chunked(chunk_size = 1) {
|
||||
const f = this.#f;
|
||||
return new Query((o) => f({ chunk_size, ...o }));
|
||||
@@ -386,7 +470,7 @@ export class Query<T = Row>
|
||||
});
|
||||
}
|
||||
|
||||
parse<S extends ObjectType>(
|
||||
parse<S extends v.ObjectType>(
|
||||
type: S,
|
||||
{ mode = "strip" }: { mode?: "passthrough" | "strict" | "strip" } = {}
|
||||
) {
|
||||
@@ -399,26 +483,25 @@ export class Query<T = Row>
|
||||
return this.#f(options);
|
||||
}
|
||||
|
||||
async first(): Promise<Result<T>> {
|
||||
const { rows, tag } = await this.collect(1);
|
||||
if (!rows.length) throw new Error(`expected one row, got none instead`);
|
||||
const row = rows[0];
|
||||
return Object.assign([row] as const, { row: rows[0], tag });
|
||||
async first(): Promise<T> {
|
||||
const rows = await this.collect(1);
|
||||
if (rows.length !== 0) return rows[0];
|
||||
else throw new TypeError(`expected one row, got none instead`);
|
||||
}
|
||||
|
||||
async first_or<S>(value: S): Promise<Result<T | S>> {
|
||||
const { rows, tag } = await this.collect(1);
|
||||
const row = rows.length ? rows[0] : value;
|
||||
return Object.assign([row] as const, { row: rows[0], tag });
|
||||
async first_or<S>(value: S): Promise<T | S> {
|
||||
const rows = await this.collect(1);
|
||||
return rows.length !== 0 ? rows[0] : value;
|
||||
}
|
||||
|
||||
async collect(count = Number.POSITIVE_INFINITY): Promise<Results<T>> {
|
||||
async collect(count = Number.POSITIVE_INFINITY): Promise<Rows<T>> {
|
||||
const iter = this[Symbol.asyncIterator]();
|
||||
let next;
|
||||
const rows = [];
|
||||
for (let i = 0; !(next = await iter.next()).done; ) {
|
||||
const { value: c } = next;
|
||||
for (let j = 0, n = c.length; i < count && j < n; ) rows[i++] = c[j++];
|
||||
const chunk = next.value;
|
||||
for (let j = 0, n = chunk.length; i < count && j < n; )
|
||||
rows[i++] = chunk[j++];
|
||||
}
|
||||
return Object.assign(rows, next.value, { rows });
|
||||
}
|
||||
@@ -437,8 +520,8 @@ export class Query<T = Row>
|
||||
return n;
|
||||
}
|
||||
|
||||
then<S = Results<T>, U = never>(
|
||||
f?: ((rows: Results<T>) => S | PromiseLike<S>) | null,
|
||||
then<S = Rows<T>, U = never>(
|
||||
f?: ((rows: Rows<T>) => S | PromiseLike<S>) | null,
|
||||
g?: ((reason?: unknown) => U | PromiseLike<U>) | null
|
||||
) {
|
||||
return this.collect().then(f, g);
|
||||
@@ -453,7 +536,8 @@ function str_to_stream(s: string) {
|
||||
return new ReadableStream({
|
||||
type: "bytes",
|
||||
start(c) {
|
||||
c.enqueue(to_utf8(s)), c.close();
|
||||
if (s.length !== 0) c.enqueue(to_utf8(s));
|
||||
c.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
2
ser.ts
2
ser.ts
@@ -11,7 +11,7 @@ import {
|
||||
write_i8,
|
||||
} from "./lstd.ts";
|
||||
|
||||
export class EncoderError extends Error {
|
||||
export class EncoderError extends TypeError {
|
||||
override get name() {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
228
test.ts
Normal file
228
test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import pglue, { PostgresError, SqlTypeError } from "./mod.ts";
|
||||
import { expect } from "jsr:@std/expect";
|
||||
import { toText } from "jsr:@std/streams";
|
||||
|
||||
const pool = pglue(`postgres://test:test@localhost:5432/test`, {
|
||||
runtime_params: { client_min_messages: "INFO" },
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
pool.on("log", (level, ctx, msg) => console.info(`${level}: ${msg}`, ctx));
|
||||
|
||||
Deno.test(`integers`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
const { a, b, c } = await pg.query`
|
||||
select
|
||||
${"0x100"}::int2 as a,
|
||||
${777}::int4 as b,
|
||||
${{
|
||||
[Symbol.toPrimitive](hint: string) {
|
||||
expect(hint).toBe("number");
|
||||
return "1234";
|
||||
},
|
||||
}}::int8 as c
|
||||
`.first();
|
||||
|
||||
expect(a).toBe(0x100);
|
||||
expect(b).toBe(777);
|
||||
expect(c).toBe(1234);
|
||||
|
||||
const { large } =
|
||||
await pg.query`select ${"10000000000000000"}::int8 as large`.first();
|
||||
|
||||
expect(large).toBe(10000000000000000n);
|
||||
|
||||
await expect(pg.query`select ${100000}::int2`).rejects.toThrow(SqlTypeError);
|
||||
await expect(pg.query`select ${"100000"}::text::int2`).rejects.toThrow(
|
||||
PostgresError
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test(`boolean`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
const { a, b, c } = await pg.query`
|
||||
select
|
||||
${true}::bool as a,
|
||||
${"n"}::bool as b,
|
||||
${undefined}::bool as c
|
||||
`.first();
|
||||
|
||||
expect(a).toBe(true);
|
||||
expect(b).toBe(false);
|
||||
expect(c).toBe(null);
|
||||
});
|
||||
|
||||
Deno.test(`bytea`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
const { string, array, buffer } = await pg.query`
|
||||
select
|
||||
${"hello, world"}::bytea as string,
|
||||
${[1, 2, 3, 4, 5]}::bytea as array,
|
||||
${Uint8Array.of(5, 4, 3, 2, 1)}::bytea as buffer
|
||||
`.first();
|
||||
|
||||
expect(string).toEqual(new TextEncoder().encode("hello, world"));
|
||||
expect(array).toEqual(Uint8Array.of(1, 2, 3, 4, 5));
|
||||
expect(buffer).toEqual(Uint8Array.of(5, 4, 3, 2, 1));
|
||||
});
|
||||
|
||||
Deno.test(`row`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
expect(
|
||||
(
|
||||
await pg.query`create table my_table (a text not null, b text not null, c text not null)`
|
||||
).tag
|
||||
).toBe(`CREATE TABLE`);
|
||||
|
||||
expect(
|
||||
(
|
||||
await pg.query`copy my_table from stdin`.stdin(
|
||||
`field a\tfield b\tfield c`
|
||||
)
|
||||
).tag
|
||||
).toBe(`COPY 1`);
|
||||
|
||||
const row = await pg.query`select * from my_table`.first();
|
||||
{
|
||||
// columns by name
|
||||
const { a, b, c } = row;
|
||||
expect(a).toBe("field a");
|
||||
expect(b).toBe("field b");
|
||||
expect(c).toBe("field c");
|
||||
}
|
||||
{
|
||||
// columns by index
|
||||
const [a, b, c] = row;
|
||||
expect(a).toBe("field a");
|
||||
expect(b).toBe("field b");
|
||||
expect(c).toBe("field c");
|
||||
}
|
||||
|
||||
const { readable, writable } = new TransformStream<Uint8Array>(
|
||||
{},
|
||||
new ByteLengthQueuingStrategy({ highWaterMark: 4096 }),
|
||||
new ByteLengthQueuingStrategy({ highWaterMark: 4096 })
|
||||
);
|
||||
await pg.query`copy my_table to stdout`.stdout(writable);
|
||||
expect(await toText(readable)).toBe(`field a\tfield b\tfield c\n`);
|
||||
});
|
||||
|
||||
Deno.test(`sql injection`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
const input = `injection'); drop table users; --`;
|
||||
|
||||
expect((await pg.query`create table users (name text not null)`).tag).toBe(
|
||||
`CREATE TABLE`
|
||||
);
|
||||
|
||||
expect((await pg.query`insert into users (name) values (${input})`).tag).toBe(
|
||||
`INSERT 0 1`
|
||||
);
|
||||
|
||||
const { name } = await pg.query<{ name: string }>`
|
||||
select name from users
|
||||
`.first();
|
||||
|
||||
expect(name).toBe(input);
|
||||
});
|
||||
|
||||
Deno.test(`listen/notify`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
const sent: string[] = [];
|
||||
|
||||
await using ch = await pg.listen(`my channel`, (payload) => {
|
||||
expect(payload).toBe(sent.shift());
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const payload = `test payload ${i}`;
|
||||
sent.push(payload);
|
||||
await ch.notify(payload);
|
||||
}
|
||||
|
||||
expect(sent.length).toBe(0);
|
||||
});
|
||||
|
||||
Deno.test(`transactions`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
|
||||
await pg.begin(async (pg) => {
|
||||
await pg.begin(async (pg, tx) => {
|
||||
await pg.query`create table my_table (field text not null)`;
|
||||
await tx.rollback();
|
||||
});
|
||||
|
||||
await expect(pg.query`select * from my_table`).rejects.toThrow(
|
||||
PostgresError
|
||||
);
|
||||
});
|
||||
|
||||
await expect(pg.query`select * from my_table`).rejects.toThrow(PostgresError);
|
||||
|
||||
await pg.begin(async (pg) => {
|
||||
await pg.begin(async (pg, tx) => {
|
||||
await pg.begin(async (pg, tx) => {
|
||||
await pg.begin(async (pg) => {
|
||||
await pg.query`create table my_table (field text not null)`;
|
||||
});
|
||||
await tx.commit();
|
||||
});
|
||||
|
||||
expect(await pg.query`select * from my_table`.count()).toBe(0);
|
||||
await tx.rollback();
|
||||
});
|
||||
|
||||
await expect(pg.query`select * from my_table`).rejects.toThrow(
|
||||
PostgresError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test(`streaming`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
await pg.query`create table my_table (field text not null)`;
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await pg.query`insert into my_table (field) values (${i})`;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for await (const chunk of pg.query`select * from my_table`.chunked(5)) {
|
||||
expect(chunk.length).toBe(5);
|
||||
for (const row of chunk) expect(row.field).toBe(`${i++}`);
|
||||
}
|
||||
|
||||
expect(i).toBe(20);
|
||||
});
|
||||
|
||||
Deno.test(`simple`, async () => {
|
||||
await using pg = await pool.connect();
|
||||
await using _tx = await pg.begin();
|
||||
|
||||
const rows = await pg.query`
|
||||
create table my_table (field text not null);
|
||||
insert into my_table (field) values ('one'), ('two'), ('three');
|
||||
select * from my_table;
|
||||
select * from my_table where field = 'two';
|
||||
`.simple();
|
||||
|
||||
expect(rows.length).toBe(4);
|
||||
|
||||
const [{ field: a }, { field: b }, { field: c }, { field: d }] = rows;
|
||||
expect(a).toBe("one");
|
||||
expect(b).toBe("two");
|
||||
expect(c).toBe("three");
|
||||
expect(d).toBe("two");
|
||||
});
|
||||
Reference in New Issue
Block a user