Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
33b5158327 | |||
7f3e3b236b | |||
3d65dcecf2 | |||
90dc51a914 | |||
a1b66c4c48 | |||
119c06565c | |||
d959a80678 | |||
c2ff6b4359 | |||
a4c0055c79 | |||
00002525e4 | |||
328cc63536 | |||
29b79f25c0 | |||
29b2796627 | |||
02f8098811 | |||
da7f7e12f3 | |||
6f9e9770cf | |||
137422601b |
80
README.md
80
README.md
@ -14,9 +14,9 @@ The glue for TypeScript to PostgreSQL.
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import pglue from "https://git.lua.re/luaneko/pglue/raw/tag/v0.1.3/mod.ts";
|
import pglue from "https://git.lua.re/luaneko/pglue/raw/tag/v0.3.3/mod.ts";
|
||||||
// ...or from github:
|
// ...or from github:
|
||||||
import pglue from "https://raw.githubusercontent.com/luaneko/pglue/refs/tags/v0.1.3/mod.ts";
|
import pglue from "https://raw.githubusercontent.com/luaneko/pglue/refs/tags/v0.3.3/mod.ts";
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@ -25,13 +25,13 @@ TODO: Write the documentation in more detail here.
|
|||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
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.
|
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 PostgreSQL 17.1 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`:
|
Query `select * from pg_type`:
|
||||||
|
|
||||||
```log
|
```
|
||||||
CPU | Common KVM Processor v2.0
|
CPU | Common KVM Processor v2.0
|
||||||
Runtime | Deno 2.1.4 (x86_64-unknown-linux-gnu)
|
Runtime | Deno 2.1.4 (x86_64-unknown-linux-gnu)
|
||||||
|
|
||||||
@ -39,78 +39,78 @@ benchmark time/iter (avg) iter/s (min … max) p75
|
|||||||
--------------- ----------------------------- --------------------- --------------------------
|
--------------- ----------------------------- --------------------- --------------------------
|
||||||
|
|
||||||
group select n=1
|
group select n=1
|
||||||
pglue 8.3 ms 120.4 ( 7.2 ms … 14.4 ms) 8.5 ms 14.4 ms 14.4 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 … 26.5 ms) 10.7 ms 26.5 ms 26.5 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 37.1 ms 26.9 ( 33.4 ms … 41.3 ms) 38.5 ms 41.3 ms 41.3 ms
|
deno-postgres 38.9 ms 25.7 ( 23.5 ms … 51.9 ms) 40.3 ms 51.9 ms 51.9 ms
|
||||||
|
|
||||||
summary
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.30x faster than postgres-js
|
1.23x faster than postgres.js
|
||||||
4.47x faster than deno-postgres
|
4.42x faster than deno-postgres
|
||||||
|
|
||||||
group select n=5
|
group select n=5
|
||||||
pglue 39.9 ms 25.1 ( 37.2 ms … 49.6 ms) 40.8 ms 49.6 ms 49.6 ms
|
pglue 40.1 ms 25.0 ( 36.1 ms … 48.2 ms) 40.7 ms 48.2 ms 48.2 ms
|
||||||
postgres-js 42.4 ms 23.6 ( 36.5 ms … 61.8 ms) 44.2 ms 61.8 ms 61.8 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 182.5 ms 5.5 (131.9 ms … 211.8 ms) 193.4 ms 211.8 ms 211.8 ms
|
deno-postgres 184.7 ms 5.4 (166.5 ms … 209.5 ms) 190.7 ms 209.5 ms 209.5 ms
|
||||||
|
|
||||||
summary
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.06x faster than postgres-js
|
1.22x faster than postgres.js
|
||||||
4.57x faster than deno-postgres
|
4.61x faster than deno-postgres
|
||||||
|
|
||||||
group select n=10
|
group select n=10
|
||||||
pglue 78.9 ms 12.7 ( 72.3 ms … 88.9 ms) 82.5 ms 88.9 ms 88.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 92.0 ms 10.9 ( 77.6 ms … 113.6 ms) 101.2 ms 113.6 ms 113.6 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 326.6 ms 3.1 (208.8 ms … 406.0 ms) 388.8 ms 406.0 ms 406.0 ms
|
deno-postgres 375.3 ms 2.7 (327.4 ms … 393.9 ms) 390.7 ms 393.9 ms 393.9 ms
|
||||||
|
|
||||||
summary
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.17x faster than postgres-js
|
1.10x faster than postgres.js
|
||||||
4.14x faster than deno-postgres
|
4.65x faster than deno-postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
Query `insert into my_table (a, b, c) values (${a}, ${b}, ${c})`:
|
Query `insert into my_table (a, b, c) values (${a}, ${b}, ${c})`:
|
||||||
|
|
||||||
```log
|
```
|
||||||
group insert n=1
|
group insert n=1
|
||||||
pglue 303.3 µs 3,297 (165.6 µs … 2.4 ms) 321.6 µs 1.1 ms 2.4 ms
|
pglue 259.2 µs 3,858 (165.4 µs … 2.8 ms) 258.0 µs 775.4 µs 2.8 ms
|
||||||
postgres-js 260.4 µs 3,840 (132.9 µs … 2.7 ms) 276.4 µs 1.1 ms 2.7 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 281.6 µs 3,552 (186.1 µs … 1.5 ms) 303.8 µs 613.6 µs 791.8 µs
|
deno-postgres 306.7 µs 3,260 (198.8 µs … 1.3 ms) 325.9 µs 1.0 ms 1.3 ms
|
||||||
|
|
||||||
summary
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.17x slower than postgres-js
|
1.10x slower than postgres.js
|
||||||
1.08x slower than deno-postgres
|
1.18x faster than deno-postgres
|
||||||
|
|
||||||
group insert n=10
|
group insert n=10
|
||||||
pglue 1.1 ms 878.5 (605.5 µs … 3.2 ms) 1.1 ms 2.2 ms 3.2 ms
|
pglue 789.7 µs 1,266 (553.2 µs … 2.7 ms) 783.4 µs 2.4 ms 2.7 ms
|
||||||
postgres-js 849.3 µs 1,177 (529.5 µs … 10.1 ms) 770.6 µs 3.0 ms 10.1 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.3 ms 439.4 ( 1.4 ms … 4.9 ms) 2.5 ms 4.1 ms 4.9 ms
|
deno-postgres 2.2 ms 458.1 ( 1.6 ms … 5.2 ms) 2.3 ms 4.8 ms 5.2 ms
|
||||||
|
|
||||||
summary
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.34x slower than postgres-js
|
1.04x slower than postgres.js
|
||||||
2.00x faster than deno-postgres
|
2.76x faster than deno-postgres
|
||||||
|
|
||||||
group insert n=100
|
group insert n=100
|
||||||
pglue 8.3 ms 121.0 ( 5.0 ms … 13.6 ms) 9.3 ms 13.6 ms 13.6 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.7 ( 9.0 ms … 26.9 ms) 14.1 ms 26.9 ms 26.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 19.8 ms 50.5 ( 14.2 ms … 31.8 ms) 22.5 ms 31.8 ms 31.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
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.58x faster than postgres-js
|
2.24x faster than postgres.js
|
||||||
2.40x faster than deno-postgres
|
3.18x faster than deno-postgres
|
||||||
|
|
||||||
group insert n=200
|
group insert n=200
|
||||||
pglue 15.1 ms 66.2 ( 9.4 ms … 21.1 ms) 16.8 ms 21.1 ms 21.1 ms
|
pglue 8.8 ms 113.4 ( 6.0 ms … 14.1 ms) 10.0 ms 14.1 ms 14.1 ms
|
||||||
postgres-js 27.8 ms 36.0 ( 22.5 ms … 39.2 ms) 30.2 ms 39.2 ms 39.2 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 40.6 ms 24.6 ( 33.5 ms … 51.4 ms) 42.2 ms 51.4 ms 51.4 ms
|
deno-postgres 37.0 ms 27.0 ( 32.0 ms … 48.1 ms) 39.4 ms 48.1 ms 48.1 ms
|
||||||
|
|
||||||
summary
|
summary
|
||||||
pglue
|
pglue
|
||||||
1.84x faster than postgres-js
|
3.20x faster than postgres.js
|
||||||
2.68x faster than deno-postgres
|
4.20x faster than deno-postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
[1]: https://github.com/porsager/postgres
|
[1]: https://github.com/porsager/postgres
|
||||||
|
4
bench.ts
4
bench.ts
@ -60,7 +60,7 @@ for (const n of [1, 5, 10]) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.bench({
|
Deno.bench({
|
||||||
name: `postgres-js`,
|
name: `postgres.js`,
|
||||||
group: `select n=${n}`,
|
group: `select n=${n}`,
|
||||||
async fn(b) {
|
async fn(b) {
|
||||||
await bench_select(b, n, () => c_pgjs`select * from pg_type`);
|
await bench_select(b, n, () => c_pgjs`select * from pg_type`);
|
||||||
@ -95,7 +95,7 @@ for (const n of [1, 10, 100, 200]) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.bench({
|
Deno.bench({
|
||||||
name: `postgres-js`,
|
name: `postgres.js`,
|
||||||
group: `insert n=${n}`,
|
group: `insert n=${n}`,
|
||||||
async fn(b) {
|
async fn(b) {
|
||||||
await c_pgjs`begin`;
|
await c_pgjs`begin`;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@luaneko/pglue",
|
"name": "@luaneko/pglue",
|
||||||
"version": "0.1.3",
|
"version": "0.3.3",
|
||||||
"exports": "./mod.ts"
|
"exports": "./mod.ts"
|
||||||
}
|
}
|
||||||
|
8
deno.lock
generated
8
deno.lock
generated
@ -465,6 +465,12 @@
|
|||||||
"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/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/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/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.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/v0.2.0/mod.ts";
|
export * from "https://git.lua.re/luaneko/lstd/raw/tag/v0.2.1/mod.ts";
|
||||||
|
132
mod.ts
132
mod.ts
@ -1,23 +1,22 @@
|
|||||||
import pg_conn_string from "npm:pg-connection-string@^2.7.0";
|
import pg_conn_str from "npm:pg-connection-string@^2.7.0";
|
||||||
import {
|
import { Pool, PoolOptions, Wire, WireOptions } from "./wire.ts";
|
||||||
type Infer,
|
|
||||||
number,
|
|
||||||
object,
|
|
||||||
record,
|
|
||||||
string,
|
|
||||||
union,
|
|
||||||
unknown,
|
|
||||||
} from "./valita.ts";
|
|
||||||
import { Pool, wire_connect, type LogLevel } from "./wire.ts";
|
|
||||||
import { sql_types, type SqlTypeMap } from "./query.ts";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
Wire,
|
||||||
|
WireOptions,
|
||||||
WireError,
|
WireError,
|
||||||
|
Pool,
|
||||||
|
PoolOptions,
|
||||||
PostgresError,
|
PostgresError,
|
||||||
|
type Postgres,
|
||||||
|
type WireEvents,
|
||||||
|
type PoolEvents,
|
||||||
type LogLevel,
|
type LogLevel,
|
||||||
|
type Parameters,
|
||||||
type Transaction,
|
type Transaction,
|
||||||
type Channel,
|
type Channel,
|
||||||
type Parameters,
|
type ChannelEvents,
|
||||||
|
type NotificationHandler,
|
||||||
} from "./wire.ts";
|
} from "./wire.ts";
|
||||||
export {
|
export {
|
||||||
type SqlFragment,
|
type SqlFragment,
|
||||||
@ -25,96 +24,49 @@ export {
|
|||||||
type SqlTypeMap,
|
type SqlTypeMap,
|
||||||
SqlTypeError,
|
SqlTypeError,
|
||||||
sql,
|
sql,
|
||||||
|
sql_types,
|
||||||
|
sql_format,
|
||||||
is_sql,
|
is_sql,
|
||||||
Query,
|
Query,
|
||||||
type Row,
|
|
||||||
type CommandResult,
|
|
||||||
type Result,
|
type Result,
|
||||||
type Results,
|
type Row,
|
||||||
type ResultStream,
|
type Rows,
|
||||||
|
type RowStream,
|
||||||
} from "./query.ts";
|
} from "./query.ts";
|
||||||
|
|
||||||
export type Options = {
|
export default function postgres(
|
||||||
host?: string;
|
s: string,
|
||||||
port?: number | string;
|
options: Partial<PoolOptions> = {}
|
||||||
user?: string;
|
) {
|
||||||
password?: string;
|
return new Pool(PoolOptions.parse(parse_conn(s, options), { mode: "strip" }));
|
||||||
database?: string | null;
|
}
|
||||||
max_connections?: number;
|
|
||||||
idle_timeout?: number;
|
|
||||||
runtime_params?: Record<string, string>;
|
|
||||||
types?: SqlTypeMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ParsedOptions = Infer<typeof ParsedOptions>;
|
postgres.connect = connect;
|
||||||
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) {
|
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 {
|
const {
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
database,
|
database,
|
||||||
ssl: _ssl, // TODO:
|
ssl: _ssl, // TODO: ssl support
|
||||||
...runtime_params
|
...runtime_params
|
||||||
} = pg_conn_string.parse(s);
|
} = s ? pg_conn_str.parse(s) : {};
|
||||||
|
|
||||||
const { PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, USER } =
|
return {
|
||||||
Deno.env.toObject();
|
...options,
|
||||||
|
host: options.host ?? host,
|
||||||
return ParsedOptions.parse({
|
port: options.port ?? port,
|
||||||
...opts,
|
user: options.user ?? user,
|
||||||
host: opts.host ?? host ?? PGHOST ?? undefined,
|
password: options.password ?? password,
|
||||||
port: opts.port ?? port ?? PGPORT ?? undefined,
|
database: options.database ?? database,
|
||||||
user: opts.user ?? user ?? PGUSER ?? USER ?? undefined,
|
runtime_params: { ...runtime_params, ...options.runtime_params },
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
138
query.ts
138
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";
|
import { from_hex, to_hex, to_utf8 } from "./lstd.ts";
|
||||||
|
|
||||||
export const sql_format = Symbol.for(`re.lua.pglue.sql_format`);
|
export const sql_format = Symbol.for(`re.lua.pglue.sql_format`);
|
||||||
@ -168,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 = {
|
export const int2: SqlType = {
|
||||||
input(s) {
|
input(s) {
|
||||||
const n = Number(s);
|
const n = Number(s);
|
||||||
@ -201,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 = {
|
export const int8: SqlType = {
|
||||||
input(s) {
|
input(s) {
|
||||||
const n = BigInt(s);
|
const n = BigInt(s);
|
||||||
@ -214,14 +247,36 @@ export const int8: SqlType = {
|
|||||||
else if (typeof x === "number" || typeof x === "bigint") n = x;
|
else if (typeof x === "number" || typeof x === "bigint") n = x;
|
||||||
else if (typeof x === "string") n = BigInt(x);
|
else if (typeof x === "string") n = BigInt(x);
|
||||||
else n = Number(x);
|
else n = Number(x);
|
||||||
if (Number.isInteger(n)) {
|
if (
|
||||||
if (-9007199254740991 <= n && n <= 9007199254740991) return n.toString();
|
(typeof n === "number" && Number.isSafeInteger(n)) ||
|
||||||
else throw new SqlTypeError(`unsafe int8 output '${x}'`);
|
(typeof n === "bigint" &&
|
||||||
} else if (typeof n === "bigint") {
|
-9223372036854775808n <= n &&
|
||||||
if (-9223372036854775808n <= n && n <= 9223372036854775807n)
|
n <= 9223372036854775807n)
|
||||||
return n.toString();
|
) {
|
||||||
}
|
return n.toString();
|
||||||
throw new SqlTypeError(`invalid int8 output '${x}'`);
|
} 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}'`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -305,60 +360,63 @@ export const json: SqlType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const sql_types: SqlTypeMap = {
|
export const sql_types: SqlTypeMap = {
|
||||||
|
0: text,
|
||||||
16: bool, // bool
|
16: bool, // bool
|
||||||
25: text, // text
|
17: bytea, // bytea
|
||||||
|
18: char, // char
|
||||||
|
19: text, // name
|
||||||
|
20: int8, // int8
|
||||||
21: int2, // int2
|
21: int2, // int2
|
||||||
23: int4, // int4
|
23: int4, // int4
|
||||||
20: int8, // int8
|
25: text, // text
|
||||||
26: int8, // oid
|
26: uint4, // oid
|
||||||
|
28: uint4, // xid
|
||||||
|
29: uint4, // cid
|
||||||
|
114: json, // json
|
||||||
700: float4, // float4
|
700: float4, // float4
|
||||||
701: float8, // float8
|
701: float8, // float8
|
||||||
1082: timestamptz, // date
|
1082: timestamptz, // date
|
||||||
1114: timestamptz, // timestamp
|
1114: timestamptz, // timestamp
|
||||||
1184: timestamptz, // timestamptz
|
1184: timestamptz, // timestamptz
|
||||||
17: bytea, // bytea
|
|
||||||
114: json, // json
|
|
||||||
3802: json, // jsonb
|
3802: json, // jsonb
|
||||||
|
5069: uint8, // xid8
|
||||||
};
|
};
|
||||||
|
|
||||||
sql.types = sql_types;
|
sql.types = sql_types;
|
||||||
|
|
||||||
type ReadonlyTuple<T extends readonly unknown[]> = readonly [...T];
|
export interface Result {
|
||||||
|
|
||||||
export interface CommandResult {
|
|
||||||
readonly tag: string;
|
readonly tag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Result<T> extends CommandResult, ReadonlyTuple<[T]> {
|
export interface Rows<T> extends Result, ReadonlyArray<T> {
|
||||||
readonly row: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Results<T> extends CommandResult, ReadonlyArray<T> {
|
|
||||||
readonly rows: ReadonlyArray<T>;
|
readonly rows: ReadonlyArray<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultStream<T>
|
export interface RowStream<T> extends AsyncIterable<T[], Result, void> {}
|
||||||
extends AsyncIterable<T[], CommandResult, void> {}
|
|
||||||
|
|
||||||
export interface Row extends Iterable<unknown, void, void> {
|
export interface Row extends Iterable<unknown, void, void> {
|
||||||
[column: string]: unknown;
|
[column: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
|
readonly simple: boolean;
|
||||||
readonly chunk_size: number;
|
readonly chunk_size: number;
|
||||||
readonly stdin: ReadableStream<Uint8Array> | null;
|
readonly stdin: ReadableStream<Uint8Array> | null;
|
||||||
readonly stdout: WritableStream<Uint8Array> | null;
|
readonly stdout: WritableStream<Uint8Array> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Query<T = Row>
|
export class Query<T = Row> implements PromiseLike<Rows<T>>, RowStream<T> {
|
||||||
implements PromiseLike<Results<T>>, ResultStream<T>
|
|
||||||
{
|
|
||||||
readonly #f;
|
readonly #f;
|
||||||
|
|
||||||
constructor(f: (options: Partial<QueryOptions>) => ResultStream<T>) {
|
constructor(f: (options: Partial<QueryOptions>) => RowStream<T>) {
|
||||||
this.#f = f;
|
this.#f = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
simple(simple = true) {
|
||||||
|
const f = this.#f;
|
||||||
|
return new Query((o) => f({ simple, ...o }));
|
||||||
|
}
|
||||||
|
|
||||||
chunked(chunk_size = 1) {
|
chunked(chunk_size = 1) {
|
||||||
const f = this.#f;
|
const f = this.#f;
|
||||||
return new Query((o) => f({ chunk_size, ...o }));
|
return new Query((o) => f({ chunk_size, ...o }));
|
||||||
@ -412,7 +470,7 @@ export class Query<T = Row>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parse<S extends ObjectType>(
|
parse<S extends v.ObjectType>(
|
||||||
type: S,
|
type: S,
|
||||||
{ mode = "strip" }: { mode?: "passthrough" | "strict" | "strip" } = {}
|
{ mode = "strip" }: { mode?: "passthrough" | "strict" | "strip" } = {}
|
||||||
) {
|
) {
|
||||||
@ -425,20 +483,18 @@ export class Query<T = Row>
|
|||||||
return this.#f(options);
|
return this.#f(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async first(): Promise<Result<T>> {
|
async first(): Promise<T> {
|
||||||
const { rows, tag } = await this.collect(1);
|
const rows = await this.collect(1);
|
||||||
if (!rows.length) throw new TypeError(`expected one row, got none instead`);
|
if (rows.length !== 0) return rows[0];
|
||||||
const row = rows[0];
|
else throw new TypeError(`expected one row, got none instead`);
|
||||||
return Object.assign([row] as const, { row: rows[0], tag });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async first_or<S>(value: S): Promise<Result<T | S>> {
|
async first_or<S>(value: S): Promise<T | S> {
|
||||||
const { rows, tag } = await this.collect(1);
|
const rows = await this.collect(1);
|
||||||
const row = rows.length ? rows[0] : value;
|
return rows.length !== 0 ? rows[0] : value;
|
||||||
return Object.assign([row] as const, { row: rows[0], tag });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async collect(count = Number.POSITIVE_INFINITY): Promise<Results<T>> {
|
async collect(count = Number.POSITIVE_INFINITY): Promise<Rows<T>> {
|
||||||
const iter = this[Symbol.asyncIterator]();
|
const iter = this[Symbol.asyncIterator]();
|
||||||
let next;
|
let next;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
@ -464,8 +520,8 @@ export class Query<T = Row>
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
then<S = Results<T>, U = never>(
|
then<S = Rows<T>, U = never>(
|
||||||
f?: ((rows: Results<T>) => S | PromiseLike<S>) | null,
|
f?: ((rows: Rows<T>) => S | PromiseLike<S>) | null,
|
||||||
g?: ((reason?: unknown) => U | PromiseLike<U>) | null
|
g?: ((reason?: unknown) => U | PromiseLike<U>) | null
|
||||||
) {
|
) {
|
||||||
return this.collect().then(f, g);
|
return this.collect().then(f, g);
|
||||||
|
2
ser.ts
2
ser.ts
@ -11,7 +11,7 @@ import {
|
|||||||
write_i8,
|
write_i8,
|
||||||
} from "./lstd.ts";
|
} from "./lstd.ts";
|
||||||
|
|
||||||
export class EncoderError extends Error {
|
export class EncoderError extends TypeError {
|
||||||
override get name() {
|
override get name() {
|
||||||
return this.constructor.name;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
|
73
test.ts
73
test.ts
@ -2,21 +2,18 @@ import pglue, { PostgresError, SqlTypeError } from "./mod.ts";
|
|||||||
import { expect } from "jsr:@std/expect";
|
import { expect } from "jsr:@std/expect";
|
||||||
import { toText } from "jsr:@std/streams";
|
import { toText } from "jsr:@std/streams";
|
||||||
|
|
||||||
async function connect() {
|
const pool = pglue(`postgres://test:test@localhost:5432/test`, {
|
||||||
const pg = await pglue.connect(`postgres://test:test@localhost:5432/test`, {
|
runtime_params: { client_min_messages: "INFO" },
|
||||||
runtime_params: { client_min_messages: "INFO" },
|
verbose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return pg.on("log", (_level, ctx, msg) => {
|
pool.on("log", (level, ctx, msg) => console.info(`${level}: ${msg}`, ctx));
|
||||||
console.info(`${msg}`, ctx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Deno.test(`integers`, async () => {
|
Deno.test(`integers`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
await using _tx = await pg.begin();
|
await using _tx = await pg.begin();
|
||||||
|
|
||||||
const [{ a, b, c }] = await pg.query`
|
const { a, b, c } = await pg.query`
|
||||||
select
|
select
|
||||||
${"0x100"}::int2 as a,
|
${"0x100"}::int2 as a,
|
||||||
${777}::int4 as b,
|
${777}::int4 as b,
|
||||||
@ -32,7 +29,7 @@ Deno.test(`integers`, async () => {
|
|||||||
expect(b).toBe(777);
|
expect(b).toBe(777);
|
||||||
expect(c).toBe(1234);
|
expect(c).toBe(1234);
|
||||||
|
|
||||||
const [{ large }] =
|
const { large } =
|
||||||
await pg.query`select ${"10000000000000000"}::int8 as large`.first();
|
await pg.query`select ${"10000000000000000"}::int8 as large`.first();
|
||||||
|
|
||||||
expect(large).toBe(10000000000000000n);
|
expect(large).toBe(10000000000000000n);
|
||||||
@ -44,10 +41,10 @@ Deno.test(`integers`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`boolean`, async () => {
|
Deno.test(`boolean`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
await using _tx = await pg.begin();
|
await using _tx = await pg.begin();
|
||||||
|
|
||||||
const [{ a, b, c }] = await pg.query`
|
const { a, b, c } = await pg.query`
|
||||||
select
|
select
|
||||||
${true}::bool as a,
|
${true}::bool as a,
|
||||||
${"n"}::bool as b,
|
${"n"}::bool as b,
|
||||||
@ -60,10 +57,10 @@ Deno.test(`boolean`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`bytea`, async () => {
|
Deno.test(`bytea`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
await using _tx = await pg.begin();
|
await using _tx = await pg.begin();
|
||||||
|
|
||||||
const [{ string, array, buffer }] = await pg.query`
|
const { string, array, buffer } = await pg.query`
|
||||||
select
|
select
|
||||||
${"hello, world"}::bytea as string,
|
${"hello, world"}::bytea as string,
|
||||||
${[1, 2, 3, 4, 5]}::bytea as array,
|
${[1, 2, 3, 4, 5]}::bytea as array,
|
||||||
@ -76,7 +73,7 @@ Deno.test(`bytea`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`row`, async () => {
|
Deno.test(`row`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
await using _tx = await pg.begin();
|
await using _tx = await pg.begin();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@ -93,7 +90,7 @@ Deno.test(`row`, async () => {
|
|||||||
).tag
|
).tag
|
||||||
).toBe(`COPY 1`);
|
).toBe(`COPY 1`);
|
||||||
|
|
||||||
const [row] = await pg.query`select * from my_table`.first();
|
const row = await pg.query`select * from my_table`.first();
|
||||||
{
|
{
|
||||||
// columns by name
|
// columns by name
|
||||||
const { a, b, c } = row;
|
const { a, b, c } = row;
|
||||||
@ -119,7 +116,7 @@ Deno.test(`row`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`sql injection`, async () => {
|
Deno.test(`sql injection`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
await using _tx = await pg.begin();
|
await using _tx = await pg.begin();
|
||||||
|
|
||||||
const input = `injection'); drop table users; --`;
|
const input = `injection'); drop table users; --`;
|
||||||
@ -132,15 +129,15 @@ Deno.test(`sql injection`, async () => {
|
|||||||
`INSERT 0 1`
|
`INSERT 0 1`
|
||||||
);
|
);
|
||||||
|
|
||||||
const [{ name }] = await pg.query<{ name: string }>`
|
const { name } = await pg.query<{ name: string }>`
|
||||||
select name from users
|
select name from users
|
||||||
`.first();
|
`.first();
|
||||||
|
|
||||||
expect(name).toBe(input);
|
expect(name).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`pubsub`, async () => {
|
Deno.test(`listen/notify`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
const sent: string[] = [];
|
const sent: string[] = [];
|
||||||
|
|
||||||
await using ch = await pg.listen(`my channel`, (payload) => {
|
await using ch = await pg.listen(`my channel`, (payload) => {
|
||||||
@ -152,10 +149,12 @@ Deno.test(`pubsub`, async () => {
|
|||||||
sent.push(payload);
|
sent.push(payload);
|
||||||
await ch.notify(payload);
|
await ch.notify(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(sent.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`transactions`, async () => {
|
Deno.test(`transactions`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
|
|
||||||
await pg.begin(async (pg) => {
|
await pg.begin(async (pg) => {
|
||||||
await pg.begin(async (pg, tx) => {
|
await pg.begin(async (pg, tx) => {
|
||||||
@ -190,20 +189,40 @@ Deno.test(`transactions`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(`streaming`, async () => {
|
Deno.test(`streaming`, async () => {
|
||||||
await using pg = await connect();
|
await using pg = await pool.connect();
|
||||||
await using _tx = await pg.begin();
|
await using _tx = await pg.begin();
|
||||||
|
|
||||||
await pg.query`create table my_table (field text not null)`;
|
await pg.query`create table my_table (field text not null)`;
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
await pg.query`insert into my_table (field) values (${i})`;
|
await pg.query`insert into my_table (field) values (${i})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for await (const chunk of pg.query`select * from my_table`.chunked(10)) {
|
for await (const chunk of pg.query`select * from my_table`.chunked(5)) {
|
||||||
expect(chunk.length).toBe(10);
|
expect(chunk.length).toBe(5);
|
||||||
for (const row of chunk) expect(row.field).toBe(`${i++}`);
|
for (const row of chunk) expect(row.field).toBe(`${i++}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(i).toBe(100);
|
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");
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user