33 Commits

Author SHA1 Message Date
33b5158327 Simply Postgres interface 2025-01-13 14:13:00 +11:00
7f3e3b236b Export more things in mod.ts 2025-01-13 13:54:31 +11:00
3d65dcecf2 Add verbose protocol logging 2025-01-13 11:00:33 +11:00
90dc51a914 Rewrite wire reconnection logic 2025-01-13 01:08:18 +11:00
a1b66c4c48 Explicitly type postgres function type parameters 2025-01-12 06:50:32 +11:00
119c06565c Rewrite options handling 2025-01-12 06:36:15 +11:00
d959a80678 Fix write_copy_in not being awaited in simple query 2025-01-12 03:35:40 +11:00
c2ff6b4359 Add more postgres utility wrappers 2025-01-12 03:28:07 +11:00
a4c0055c79 Return row value direct in .first() and .first_or(x) 2025-01-12 01:25:55 +11:00
00002525e4 Implement wire automatic reconnection 2025-01-12 01:09:34 +11:00
328cc63536 Update benchmark results 2025-01-12 00:26:38 +11:00
29b79f25c0 Fix close event not firing on wire 2025-01-12 00:23:26 +11:00
29b2796627 Log query inside pipeline handler 2025-01-11 07:24:23 +11:00
02f8098811 Code quality check 2025-01-11 07:21:44 +11:00
da7f7e12f3 Implement wire reconnect support 2025-01-11 06:02:32 +11:00
6f9e9770cf Ensure pipeline is locked during authentication phase 2025-01-11 02:43:42 +11:00
137422601b Implement simple query support 2025-01-11 02:00:05 +11:00
3793e14f50 Add v prefix to all version tags 2025-01-11 00:53:45 +11:00
b194397645 Optimise query request write 2025-01-11 00:46:17 +11:00
858b7a95f3 Add streaming query testing code 2025-01-11 00:39:00 +11:00
a88da00dec Update version tag in readme 2025-01-11 00:28:51 +11:00
cefe14b9dc Ensure stdout writable stream is always closed 2025-01-11 00:28:14 +11:00
4e68e34fd0 Add more testing code 2025-01-11 00:15:19 +11:00
826190ecc9 Add github import url 2025-01-10 20:35:32 +11:00
72749e5841 Update readme 2025-01-10 20:32:06 +11:00
b9829bc70d Update readme 2025-01-10 19:56:16 +11:00
9eecf29bc5 Update readme 2025-01-10 19:53:04 +11:00
8964cb342e Update benchmarks 2025-01-10 19:32:41 +11:00
eeed8b2f66 Rewrite type handling to be more performant 2025-01-10 17:30:04 +11:00
b72c548c33 Rename byten_lp to bytes_lp 2025-01-10 04:25:50 +11:00
60899d1a41 Update lstd to 0.2.0 2025-01-10 04:11:17 +11:00
bdebb22a0e Use BinaryLike interface 2025-01-09 04:48:19 +11:00
5dadd7c5a2 Fix integer encoding and decoding 2025-01-09 04:37:30 +11:00
12 changed files with 1596 additions and 1047 deletions

121
README.md
View File

@@ -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.3/mod.ts";
// ...or from github:
import pglue from "https://raw.githubusercontent.com/luaneko/pglue/refs/tags/v0.3.3/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

View File

@@ -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";

View File

@@ -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.3",
"exports": "./mod.ts"
}

51
deno.lock generated
View File

@@ -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,17 +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.1.2/async.ts": "ec1a2d25af2320f136b8648b25b590b7b6603525474f0d10b3ebf2215a5c23e5",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.2/bytes.ts": "39d4c08f6446041f1d078bbf285187c337d49f853b20ec637cf1516fae8b3729",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.2/events.ts": "51bf13b819d1c4af792a40ff5d8d08407502d3f01d94f6b6866156f52cbe5d64",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.2/func.ts": "f1935f673365cd68939531d65ef18fe81b5d43dc795b03c34bb5ad821ab1c9ff",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.2/jit.ts": "1b7eec61ece15c05146446972a59d8d5787d4ba53ca1194f4450134d66a65f91",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.2/mod.ts": "d7ef832245676b097c4fb7829c5cb2df80c02d2bd28767168c4f83bc309c9b1a",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/async.ts": "20bc54c7260c2d2cd27ffcca33b903dde57a3a3635386d8e0c6baca4b253ae4e",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/bytes.ts": "39d4c08f6446041f1d078bbf285187c337d49f853b20ec637cf1516fae8b3729",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/events.ts": "c4f2c856cbc7ac5d93b9af9b83d9550db7427cead32514a10424082e492005ae",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/func.ts": "f1935f673365cd68939531d65ef18fe81b5d43dc795b03c34bb5ad821ab1c9ff",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/jit.ts": "260ab418fbc55a5dec594f023c84d36f8d420fd3239e3d27648cba1b9a0e05b1",
"https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/mod.ts": "dd9271f4e5aae4bfb1ec6b0800697ded12e4178af915acb2b96b97614ae8c8d9"
"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"
}
}

View File

@@ -1 +1 @@
export * from "https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/mod.ts";
export * from "https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/mod.ts";

140
mod.ts
View File

@@ -1,126 +1,72 @@
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 { type FromSql, type ToSql, from_sql, to_sql } from "./sql.ts";
import pg_conn_str from "npm:pg-connection-string@^2.7.0";
import { Pool, PoolOptions, Wire, WireOptions } from "./wire.ts";
export {
Wire,
WireOptions,
WireError,
Pool,
PoolOptions,
PostgresError,
type Postgres,
type WireEvents,
type PoolEvents,
type LogLevel,
type Parameters,
type Transaction,
type Channel,
type Parameters,
type ChannelEvents,
type NotificationHandler,
} from "./wire.ts";
export {
type SqlFragment,
type FromSql,
type ToSql,
SqlValue,
type SqlType,
type SqlTypeMap,
SqlTypeError,
sql,
sql_types,
sql_format,
is_sql,
} from "./sql.ts";
export {
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>;
from_sql?: FromSql;
to_sql?: ToSql;
};
export default function postgres(
s: string,
options: Partial<PoolOptions> = {}
) {
return new Pool(PoolOptions.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),
from_sql: unknown()
.assert((s): s is FromSql => typeof s === "function")
.optional(() => from_sql),
to_sql: unknown()
.assert((s): s is ToSql => typeof s === "function")
.optional(() => to_sql),
});
postgres.connect = connect;
function parse_opts(s: string, options: Options) {
export async function connect(s: string, options: Partial<WireOptions> = {}) {
return await new Wire(
WireOptions.parse(parse_conn(s, options), { mode: "strip" })
).connect();
}
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({
return {
...options,
host: options.host ?? host ?? PGHOST ?? undefined,
port: options.port ?? port ?? PGPORT ?? undefined,
user: options.user ?? user ?? PGUSER ?? USER ?? undefined,
password: options.password ?? password ?? PGPASSWORD ?? undefined,
database: options.database ?? database ?? PGDATABASE ?? undefined,
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 },
});
}
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));
}
postgres.connect = connect;
export type PostgresEvents = {
log(level: LogLevel, ctx: object, msg: string): void;
};
export class Postgres extends Pool {
readonly #options;
constructor(options: ParsedOptions) {
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));
}
};
}

View File

@@ -1,13 +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) => {
await pg.begin(async (pg) => {
console.log(await pg.query`select * from pg_user`);
});
});

512
query.ts
View File

@@ -1,96 +1,422 @@
import type { ObjectType } from "./valita.ts";
import { from_utf8, jit, to_utf8 } from "./lstd.ts";
import { type FromSql, SqlValue } from "./sql.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`);
export interface SqlFragment {
[sql_format](f: SqlFormatter): void;
}
export interface SqlFormatter {
query: string;
params: unknown[];
}
export function is_sql(x: unknown): x is SqlFragment {
return typeof x === "object" && x !== null && sql_format in x;
}
export function sql(
{ raw: s }: TemplateStringsArray,
...xs: unknown[]
): SqlFragment {
return {
[sql_format](fmt) {
for (let i = 0, n = s.length; i < n; i++) {
if (i !== 0) fmt_format(fmt, xs[i - 1]);
fmt.query += s[i];
}
},
};
}
export function fmt_write(fmt: SqlFormatter, s: string | SqlFragment) {
is_sql(s) ? s[sql_format](fmt) : (fmt.query += s);
}
export function fmt_format(fmt: SqlFormatter, x: unknown) {
is_sql(x) ? x[sql_format](fmt) : fmt_enclose(fmt, x);
}
export function fmt_enclose(fmt: SqlFormatter, x: unknown) {
const { params } = fmt;
params.push(x), (fmt.query += `$` + params.length);
}
sql.format = format;
sql.raw = raw;
sql.ident = ident;
sql.fragment = fragment;
sql.map = map;
sql.array = array;
sql.row = row;
export function format(sql: SqlFragment) {
const fmt: SqlFormatter = { query: "", params: [] };
return sql[sql_format](fmt), fmt;
}
export function raw(s: string): SqlFragment;
export function raw(s: TemplateStringsArray, ...xs: unknown[]): SqlFragment;
export function raw(
s: TemplateStringsArray | string,
...xs: unknown[]
): SqlFragment {
s = typeof s === "string" ? s : String.raw(s, ...xs);
return {
[sql_format](fmt) {
fmt.query += s;
},
};
}
export function ident(s: string): SqlFragment;
export function ident(s: TemplateStringsArray, ...xs: unknown[]): SqlFragment;
export function ident(s: TemplateStringsArray | string, ...xs: unknown[]) {
s = typeof s === "string" ? s : String.raw(s, ...xs);
return raw`"${s.replaceAll('"', '""')}"`;
}
export function fragment(
sep: string | SqlFragment,
...xs: unknown[]
): SqlFragment {
return {
[sql_format](fmt) {
for (let i = 0, n = xs.length; i < n; i++) {
if (i !== 0) fmt_write(fmt, sep);
fmt_format(fmt, xs[i]);
}
},
};
}
export function map<T>(
sep: string | SqlFragment,
xs: Iterable<T>,
f: (value: T, index: number) => unknown
) {
return fragment(sep, ...Iterator.from(xs).map(f));
}
export function array(...xs: unknown[]) {
return sql`array[${fragment(", ", ...xs)}]`;
}
export function row(...xs: unknown[]) {
return sql`row(${fragment(", ", ...xs)})`;
}
export interface SqlType {
input(value: string): unknown;
output(value: unknown): string | null;
}
export interface SqlTypeMap {
readonly [oid: number]: SqlType | undefined;
}
export class SqlTypeError extends TypeError {
override get name() {
return this.constructor.name;
}
}
export const bool: SqlType = {
input(s) {
return s !== "f";
},
output(x) {
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;
},
output(x) {
if (typeof x === "undefined" || x === null) return null;
else if (typeof x === "string") return x;
else return String(x);
},
};
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);
if (Number.isInteger(n) && -32768 <= n && n <= 32767) return n;
else throw new SqlTypeError(`invalid int2 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) && -32768 <= n && n <= 32767) return n.toString();
else throw new SqlTypeError(`invalid int2 output '${x}'`);
},
};
export const int4: SqlType = {
input(s) {
const n = Number(s);
if (Number.isInteger(n) && -2147483648 <= n && n <= 2147483647) return n;
else throw new SqlTypeError(`invalid int4 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) && -2147483648 <= n && n <= 2147483647)
return n.toString();
else throw new SqlTypeError(`invalid int4 output '${x}'`);
},
};
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);
if (-9007199254740991n <= n && n <= 9007199254740991n) return Number(n);
else if (-9223372036854775808n <= n && n <= 9223372036854775807n) return n;
else throw new SqlTypeError(`invalid int8 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)) ||
(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}'`);
},
};
export const float4: SqlType = {
input(s) {
return Math.fround(Number(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.isNaN(n))
throw new SqlTypeError(`invalid float4 output '${x}'`);
}
return Math.fround(n).toString();
},
};
export const float8: SqlType = {
input(s) {
return Number(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.isNaN(n))
throw new SqlTypeError(`invalid float8 output '${x}'`);
}
return n.toString();
},
};
export const timestamptz: SqlType = {
input(s) {
const t = Date.parse(s);
if (!Number.isNaN(t)) return new Date(t);
else throw new SqlTypeError(`invalid timestamptz input '${s}'`);
},
output(x) {
let t: Date;
if (typeof x === "undefined" || x === null) return null;
else if (x instanceof Date) t = x;
else if (typeof x === "number" || typeof x === "bigint")
t = new Date(Number(x) * 1000); // unix epoch seconds
else t = new Date(String(x));
if (Number.isFinite(t.getTime())) return t.toISOString();
else throw new SqlTypeError(`invalid timestamptz output '${x}'`);
},
};
export const bytea: SqlType = {
input(s) {
if (s.startsWith(`\\x`)) return from_hex(s.slice(2));
else throw new SqlTypeError(`invalid bytea input '${s}'`);
},
output(x) {
let buf: Uint8Array;
if (typeof x === "undefined" || x === null) return null;
else if (typeof x === "string") buf = to_utf8(x);
else if (x instanceof Uint8Array) buf = x;
else if (x instanceof ArrayBuffer || x instanceof SharedArrayBuffer)
buf = new Uint8Array(x);
else if (Array.isArray(x) || x instanceof Array) buf = Uint8Array.from(x);
else throw new SqlTypeError(`invalid bytea output '${x}'`);
return `\\x` + to_hex(buf);
},
};
export const json: SqlType = {
input(s) {
return JSON.parse(s);
},
output(x) {
return typeof x === "undefined" ? null : JSON.stringify(x);
},
};
export const sql_types: SqlTypeMap = {
0: text,
16: bool, // bool
17: bytea, // bytea
18: char, // char
19: text, // name
20: int8, // int8
21: int2, // int2
23: int4, // int4
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
3802: json, // jsonb
5069: uint8, // xid8
};
sql.types = sql_types;
export interface Result {
readonly tag: string;
}
export interface Rows<T> extends Result, ReadonlyArray<T> {
readonly rows: ReadonlyArray<T>;
}
export interface RowStream<T> extends AsyncIterable<T[], Result, void> {}
export interface Row extends Iterable<unknown, void, void> {
[column: string]: unknown;
}
export interface RowConstructor {
new (columns: (Uint8Array | string | null)[]): Row;
}
export interface RowDescription extends ReadonlyArray<ColumnDescription> {}
export interface ColumnDescription {
readonly name: string;
readonly table_oid: number;
readonly table_column: number;
readonly type_oid: number;
readonly type_size: number;
readonly type_modifier: number;
}
export function row_ctor(from_sql: FromSql, columns: RowDescription) {
function parse(s: Uint8Array | string | null | undefined) {
if (!s && s !== "") return null;
else return from_utf8(s);
}
const Row = jit.compiled<RowConstructor>`function Row(xs) {
${jit.map(" ", columns, ({ name, type_oid }, i) => {
return jit`this[${jit.literal(name)}] = ${from_sql}(
new ${SqlValue}(${jit.literal(type_oid)}, ${parse}(xs[${jit.literal(i)}]))
);`;
})}
}`;
Row.prototype = Object.create(null, {
[Symbol.toStringTag]: {
configurable: true,
value: `Row`,
},
[Symbol.toPrimitive]: {
configurable: true,
value: function format() {
return [...this].join("\t");
},
},
[Symbol.iterator]: {
configurable: true,
value: jit.compiled`function* iter() {
${jit.map(" ", columns, ({ name }) => {
return jit`yield this[${jit.literal(name)}];`;
})}
}`,
},
});
return Row;
}
type ReadonlyTuple<T extends readonly unknown[]> = readonly [...T];
export interface CommandResult {
readonly tag: string;
}
export interface Result<T> extends CommandResult, ReadonlyTuple<[T]> {
readonly row: T;
}
export interface Results<T> extends CommandResult, ReadonlyArray<T> {
readonly rows: ReadonlyArray<T>;
}
export interface ResultStream<T>
extends AsyncIterable<T[], CommandResult, void> {}
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 }));
@@ -144,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" } = {}
) {
@@ -157,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 });
}
@@ -195,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);
@@ -211,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();
},
});
}

101
ser.ts
View File

@@ -1,6 +1,17 @@
import { encode_utf8, from_utf8, jit } from "./lstd.ts";
import {
type BinaryLike,
encode_utf8,
from_utf8,
jit,
read_i16_be,
read_i32_be,
read_i8,
write_i16_be,
write_i32_be,
write_i8,
} from "./lstd.ts";
export class EncoderError extends Error {
export class EncoderError extends TypeError {
override get name() {
return this.constructor.name;
}
@@ -30,44 +41,31 @@ export interface Encoder<T> {
export type EncoderType<E extends Encoder<unknown>> =
E extends Encoder<infer T> ? T : never;
export function sum_const_size(...ns: (number | null)[]) {
let sum = 0;
for (const n of ns) {
if (n !== null) sum += n;
else return null;
}
return sum;
}
// https://www.postgresql.org/docs/current/protocol-message-types.html#PROTOCOL-MESSAGE-TYPES
export const u8: Encoder<number> = {
export const i8: Encoder<number> = {
const_size: 1,
allocs() {
return 1;
},
encode(buf, cur, n) {
buf[cur.i++] = n & 0xff;
write_i8(buf, n, cur.i++);
},
decode(buf, cur) {
return buf[cur.i++];
return read_i8(buf, cur.i++);
},
};
export const u16: Encoder<number> = {
export const i16: Encoder<number> = {
const_size: 2,
allocs() {
return 2;
},
encode(buf, cur, n) {
let { i } = cur;
buf[i++] = (n >>> 8) & 0xff;
buf[i++] = n & 0xff;
cur.i = i;
write_i16_be(buf, n, cur.i), (cur.i += 2);
},
decode(buf, cur) {
let { i } = cur;
const n = (buf[i++] << 8) + buf[i++];
return (cur.i = i), n;
const n = read_i16_be(buf, cur.i);
return (cur.i += 2), n;
},
};
@@ -77,17 +75,11 @@ export const i32: Encoder<number> = {
return 4;
},
encode(buf, cur, n) {
let { i } = cur;
buf[i++] = (n >>> 24) & 0xff;
buf[i++] = (n >>> 16) & 0xff;
buf[i++] = (n >>> 8) & 0xff;
buf[i++] = n & 0xff;
cur.i = i;
write_i32_be(buf, n, cur.i), (cur.i += 4);
},
decode(buf, cur) {
let { i } = cur;
const n = (buf[i++] << 24) + (buf[i++] << 16) + (buf[i++] << 8) + buf[i++];
return (cur.i = i), n;
const n = read_i32_be(buf, cur.i);
return (cur.i += 4), n;
},
};
@@ -118,7 +110,21 @@ export function byten(n: number): Encoder<Uint8Array> {
};
}
export const byten_lp: Encoder<Uint8Array | string | null> = {
export const bytes: Encoder<BinaryLike> = {
const_size: null,
allocs(s) {
if (typeof s === "string") return s.length * 3;
else return s.length;
},
encode(buf, cur, s) {
cur.i += encode_utf8(s, buf.subarray(cur.i));
},
decode(buf, cur) {
return buf.subarray(cur.i, (cur.i = buf.length));
},
};
export const bytes_lp: Encoder<BinaryLike | null> = {
const_size: null,
allocs(s) {
let size = 4;
@@ -140,20 +146,6 @@ export const byten_lp: Encoder<Uint8Array | string | null> = {
},
};
export const byten_rest: Encoder<Uint8Array | string> = {
const_size: null,
allocs(s) {
if (typeof s === "string") return s.length * 3;
else return s.length;
},
encode(buf, cur, s) {
cur.i += encode_utf8(s, buf.subarray(cur.i));
},
decode(buf, cur) {
return buf.subarray(cur.i, (cur.i = buf.length));
},
};
export const cstring: Encoder<string> = {
const_size: null,
allocs(s) {
@@ -215,7 +207,7 @@ export function array<T>(
): ArrayEncoder<T> {
const { const_size } = type;
return {
const_size,
const_size: null,
allocs:
const_size !== null
? function allocs(xs: T[]) {
@@ -249,21 +241,28 @@ export interface ObjectEncoder<S extends ObjectShape>
export function object<S extends ObjectShape>(shape: S): ObjectEncoder<S> {
const keys = Object.keys(shape);
return jit.compiled`{
const_size: ${jit.literal(sum_const_size(...keys.map((k) => shape[k].const_size)))},
const_size: null,
allocs(x) {
return ${jit.if(
keys.length === 0,
jit`0`,
jit.map(" + ", keys, (k) => {
return shape[k].const_size ?? jit`${shape[k]}.allocs(x[${k}])`;
})
)};
return 0${jit.map("", keys, (k) => {
return jit` + ${shape[k]}.allocs(x[${jit.literal(k)}])`;
return jit` + ${shape[k]}.allocs(x[${k}])`;
})};
},
encode(buf, cur, x) {
${jit.map(" ", keys, (k) => {
return jit`${shape[k]}.encode(buf, cur, x[${jit.literal(k)}]);`;
return jit`${shape[k]}.encode(buf, cur, x[${k}]);`;
})}
},
decode(buf, cur) {
return {
${jit.map(", ", keys, (k) => {
return jit`[${jit.literal(k)}]: ${shape[k]}.decode(buf, cur)`;
return jit`[${k}]: ${shape[k]}.decode(buf, cur)`;
})}
};
},

386
sql.ts
View File

@@ -1,386 +0,0 @@
import { from_hex, to_hex } from "./lstd.ts";
export const sql_format = Symbol.for(`re.lua.pglue.sql_format`);
export interface SqlFragment {
[sql_format](f: SqlFormatter): void;
}
export function is_sql(x: unknown): x is SqlFragment {
return typeof x === "object" && x !== null && sql_format in x;
}
export interface FromSql {
(x: SqlValue): unknown;
}
export interface ToSql {
(x: unknown): SqlFragment;
}
export const from_sql = function from_sql(x) {
const { type, value } = x;
if (value === null) return null;
switch (type) {
case 16: // boolean
return boolean.parse(value);
case 25: // text
return text.parse(value);
case 21: // int2
return int2.parse(value);
case 23: // int4
return int4.parse(value);
case 20: // int8
case 26: // oid
return int8.parse(value);
case 700: // float4
return float4.parse(value);
case 701: // float8
return float8.parse(value);
case 1082: // date
case 1114: // timestamp
case 1184: // timestamptz
return timestamptz.parse(value);
case 17: // bytea
return bytea.parse(value);
case 114: // json
case 3802: // jsonb
return json.parse(value);
default:
return x;
}
} as FromSql;
export const to_sql = function to_sql(x) {
switch (typeof x) {
case "undefined":
return nil();
case "boolean":
return boolean(x);
case "number":
return float8(x);
case "bigint":
return int8(x);
case "string":
case "symbol":
case "function":
return text(x);
}
switch (true) {
case x === null:
return nil();
case is_sql(x):
return x;
case Array.isArray(x):
return array(...(x instanceof Array ? x : Array.from(x)));
case x instanceof Date:
return timestamptz(x);
case x instanceof Uint8Array:
case x instanceof ArrayBuffer:
case x instanceof SharedArrayBuffer:
return bytea(x);
}
throw new TypeError(`cannot convert input '${x}' to sql`);
} as ToSql;
export class SqlValue implements SqlFragment {
constructor(
readonly type: number,
readonly value: string | null
) {}
[sql_format](f: SqlFormatter) {
f.write_param(this.type, this.value);
}
[Symbol.toStringTag]() {
return `${this.constructor.name}<${this.type}>`;
}
[Symbol.toPrimitive]() {
return this.value;
}
toString() {
return String(this.value);
}
toJSON() {
return this.value;
}
}
export function value(type: number, x: unknown) {
const s = x === null || typeof x === "undefined" ? null : String(x);
return new SqlValue(type, s);
}
export class SqlFormatter {
readonly #ser;
#query = "";
#params = {
types: [] as number[],
values: [] as (string | null)[],
};
get query() {
return this.#query.trim();
}
get params() {
return this.#params;
}
constructor(serializer: ToSql) {
this.#ser = serializer;
}
write(s: string | SqlFragment) {
if (is_sql(s)) s[sql_format](this);
else this.#query += s;
}
write_param(type: number, s: string | null) {
const { types, values } = this.#params;
types.push(type), values.push(s), this.write(`$` + values.length);
}
format(x: unknown) {
this.write(is_sql(x) ? x : this.#ser(x));
}
}
export function format(sql: SqlFragment, serializer = to_sql) {
const fmt = new SqlFormatter(serializer);
return fmt.write(sql), fmt;
}
export function sql(
{ raw: s }: TemplateStringsArray,
...xs: unknown[]
): SqlFragment {
return {
[sql_format](f) {
for (let i = 0, n = s.length; i < n; i++) {
if (i !== 0) f.format(xs[i - 1]);
f.write(s[i]);
}
},
};
}
sql.value = value;
sql.format = format;
sql.raw = raw;
sql.ident = ident;
sql.fragment = fragment;
sql.map = map;
sql.array = array;
sql.row = row;
sql.null = nil;
sql.boolean = boolean;
sql.text = text;
sql.int2 = int2;
sql.int4 = int4;
sql.int8 = int8;
sql.float4 = float4;
sql.float8 = float8;
sql.timestamptz = timestamptz;
sql.bytea = bytea;
sql.json = json;
export function raw(s: TemplateStringsArray, ...xs: unknown[]): SqlFragment;
export function raw(s: string): SqlFragment;
export function raw(
s: TemplateStringsArray | string,
...xs: unknown[]
): SqlFragment {
s = typeof s === "string" ? s : String.raw(s, ...xs);
return {
[sql_format](f) {
f.write(s);
},
};
}
export function ident(s: TemplateStringsArray, ...xs: unknown[]): SqlFragment;
export function ident(s: string): SqlFragment;
export function ident(s: TemplateStringsArray | string, ...xs: unknown[]) {
s = typeof s === "string" ? s : String.raw(s, ...xs);
return raw`"${s.replaceAll('"', '""')}"`;
}
export function fragment(
sep: string | SqlFragment,
...xs: unknown[]
): SqlFragment {
return {
[sql_format](f) {
for (let i = 0, n = xs.length; i < n; i++) {
if (i !== 0) f.write(sep);
f.format(xs[i]);
}
},
};
}
export function map<T>(
sep: string | SqlFragment,
xs: Iterable<T>,
f: (value: T, index: number) => unknown
): SqlFragment {
return fragment(sep, ...Iterator.from(xs).map(f));
}
export function array(...xs: unknown[]): SqlFragment {
return sql`array[${fragment(", ", ...xs)}]`;
}
export function row(...xs: unknown[]): SqlFragment {
return sql`row(${fragment(", ", ...xs)})`;
}
boolean.oid = 16 as const;
text.oid = 25 as const;
int2.oid = 21 as const;
int4.oid = 23 as const;
int8.oid = 20 as const;
float4.oid = 700 as const;
float8.oid = 701 as const;
timestamptz.oid = 1184 as const;
bytea.oid = 17 as const;
json.oid = 114 as const;
export function nil() {
return value(0, null);
}
Object.defineProperty(nil, "name", { configurable: true, value: "null" });
export function boolean(x: unknown) {
return value(
boolean.oid,
x === null || typeof x === "undefined" ? null : x ? "t" : "f"
);
}
boolean.parse = function parse_boolean(s: string) {
return s === "t";
};
export function text(x: unknown) {
return value(text.oid, x);
}
text.parse = function parse_text(s: string) {
return s;
};
const i2_min = -32768;
const i2_max = 32767;
export function int2(x: unknown) {
return value(int2.oid, x);
}
int2.parse = function parse_int2(s: string) {
const n = Number(s);
if (Number.isInteger(n) && i2_min <= n && n <= i2_max) return n;
else throw new TypeError(`input '${s}' is not a valid int2 value`);
};
const i4_min = -2147483648;
const i4_max = 2147483647;
export function int4(x: unknown) {
return value(int4.oid, x);
}
int4.parse = function parse_int4(s: string) {
const n = Number(s);
if (Number.isInteger(n) && i4_min <= n && n <= i4_max) return n;
else throw new TypeError(`input '${s}' is not a valid int4 value`);
};
const i8_min = -9223372036854775808n;
const i8_max = 9223372036854775807n;
export function int8(x: unknown) {
return value(int8.oid, x);
}
function to_int8(n: number | bigint) {
if (typeof n === "bigint") return i8_min <= n && n <= i8_max ? n : null;
else return Number.isSafeInteger(n) ? BigInt(n) : null;
}
int8.parse = function parse_int8(s: string) {
const n = to_int8(BigInt(s));
if (n !== null) return to_float8(n) ?? n;
else throw new TypeError(`input '${s}' is not a valid int8 value`);
};
const f8_min = -9007199254740991n;
const f8_max = 9007199254740991n;
export function float4(x: unknown) {
return value(float4.oid, x);
}
export function float8(x: unknown) {
return value(float8.oid, x);
}
function to_float8(n: number | bigint) {
if (typeof n === "bigint")
return f8_min <= n && n <= f8_max ? Number(n) : null;
else return Number.isNaN(n) ? null : n;
}
float4.parse = float8.parse = function parse_float8(s: string) {
const n = to_float8(Number(s));
if (n !== null) return n;
else throw new TypeError(`input '${s}' is not a valid float8 value`);
};
export function timestamptz(x: unknown) {
if (x instanceof Date) x = x.toISOString();
else if (typeof x === "number" || typeof x === "bigint")
x = new Date(Number(x) * 1000).toISOString(); // unix epoch
return value(timestamptz.oid, x);
}
timestamptz.parse = function parse_timestamptz(s: string) {
const t = Date.parse(s);
if (!Number.isNaN(t)) return new Date(t);
else throw new TypeError(`input '${s}' is not a valid timestamptz value`);
};
export function bytea(x: Uint8Array | ArrayBufferLike | Iterable<number>) {
let buf;
if (x instanceof Uint8Array) buf = x;
else if (x instanceof ArrayBuffer || x instanceof SharedArrayBuffer)
buf = new Uint8Array(x);
else buf = Uint8Array.from(x);
return value(bytea.oid, `\\x` + to_hex(buf));
}
bytea.parse = function parse_bytea(s: string) {
if (s.startsWith(`\\x`)) return from_hex(s.slice(2));
else throw new TypeError(`input is not a valid bytea value`);
};
export function json(x: unknown) {
return value(json.oid, JSON.stringify(x) ?? null);
}
json.parse = function parse_json(s: string): unknown {
return JSON.parse(s);
};

228
test.ts Normal file
View 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");
});

1079
wire.ts

File diff suppressed because it is too large Load Diff