From 27c3f56207f103b1dc43645c2534a8e7aaf41cd9 Mon Sep 17 00:00:00 2001 From: luaneko Date: Tue, 7 Jan 2025 22:12:30 +1100 Subject: [PATCH] Initial commit --- bench.ts | 102 ++++ copy.ts | 21 + deno.json | 9 + deno.lock | 449 ++++++++++++++++ lstd.ts | 1 + mod.ts | 126 +++++ mod_test.ts | 13 + query.ts | 217 ++++++++ ser.ts | 271 ++++++++++ sql.ts | 386 ++++++++++++++ valita.ts | 1 + wire.ts | 1421 +++++++++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 3017 insertions(+) create mode 100644 bench.ts create mode 100644 copy.ts create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 lstd.ts create mode 100644 mod.ts create mode 100644 mod_test.ts create mode 100644 query.ts create mode 100644 ser.ts create mode 100644 sql.ts create mode 100644 valita.ts create mode 100644 wire.ts diff --git a/bench.ts b/bench.ts new file mode 100644 index 0000000..95265a7 --- /dev/null +++ b/bench.ts @@ -0,0 +1,102 @@ +import * as 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"; + +const c_pglue = await pglue.connect(`postgres://test:test@localhost:5432/test`); + +const c_pgjs = await postgres_js( + `postgres://test:test@localhost:5432/test` +).reserve(); + +const c_denopg = new deno_postgres.Client({ + user: "test", + database: "test", + hostname: "localhost", + password: "test", + port: 5432, +}); + +await c_denopg.connect(); + +async function bench_select( + b: Deno.BenchContext, + n: number, + q: () => PromiseLike +) { + await q(); + b.start(); + + const tasks = []; + for (let i = 0; i < n; i++) tasks.push(q()); + + await Promise.all(tasks); + b.end(); +} + +async function bench_insert( + b: Deno.BenchContext, + n: number, + q: (a: string, b: boolean, c: number) => PromiseLike +) { + await q("prepare", false, 0); + b.start(); + + const tasks = []; + for (let i = 0; i < n; i++) + tasks.push(q(i.toString(16).repeat(5), i % 3 === 0, i)); + + await Promise.all(tasks); + b.end(); +} + +for (const n of [1, 2, 5, 10]) { + Deno.bench(`pglue (select, n=${n})`, async (b) => { + await bench_select(b, n, () => c_pglue.query`select * from pg_type`); + }); + + Deno.bench(`postgres.js (select, n=${n})`, async (b) => { + await bench_select(b, n, () => c_pgjs`select * from pg_type`); + }); + + Deno.bench(`deno-postgres (select, n=${n})`, async (b) => { + await bench_select(b, n, () => c_denopg.queryArray`select * from pg_type`); + }); +} + +for (const n of [1, 2, 5, 10, 50, 100, 200]) { + Deno.bench(`pglue (insert, n=${n})`, async (b) => { + await using _tx = await c_pglue.begin(); + await c_pglue.query`create table my_table (a text not null, b boolean not null, c integer not null)`; + await bench_insert(b, n, (a, b, c) => + c_pglue.query`insert into my_table (a, b, c) values (${a}, ${b}, ${c})`.execute() + ); + }); + + Deno.bench(`postgres.js (insert, n=${n})`, async (b) => { + await c_pgjs`begin`; + try { + await c_pgjs`create table my_table (a text not null, b boolean not null, c integer not null)`; + await bench_insert(b, n, (a, b, c) => + c_pgjs`insert into my_table (a, b, c) values (${a}, ${b}, ${c})`.execute() + ); + } finally { + await c_pgjs`rollback`; + } + }); + + Deno.bench(`deno-postgres (insert, n=${n})`, async (b) => { + const tx = c_denopg.createTransaction(`my_tx`); + await tx.begin(); + try { + await tx.queryArray`create table my_table (a text not null, b boolean not null, c integer not null)`; + await bench_insert( + b, + n, + (a, b, c) => + tx.queryArray`insert into my_table (a, b, c) values (${a}, ${b}, ${c})` + ); + } finally { + await tx.rollback(); + } + }); +} diff --git a/copy.ts b/copy.ts new file mode 100644 index 0000000..17c82a9 --- /dev/null +++ b/copy.ts @@ -0,0 +1,21 @@ +// https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.2 +const rg_octal = /^\\([0-7]{1,3})/; +const rg_hex = /^\\x([0-9a-fA-F]{1,2})/; + +export function copy_fmt(cols: readonly string[]) { + let s = ""; + for (let i = 0, n = cols.length; i < n; i++) { + if (i !== 0) s += "\t"; + s += copy_fmt_escape(cols[i]); + } + return s; +} + +export function copy_fmt_escape(s: string) { + return s + .replaceAll("\\", "\\\\") + .replaceAll("\n", "\\n") + .replaceAll("\r", "\\r") + .replaceAll("\t", "\\t") + .replaceAll("\0", "\\000"); +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..22a49af --- /dev/null +++ b/deno.json @@ -0,0 +1,9 @@ +{ + "name": "@luaneko/pglue", + "version": "0.1.0", + "exports": "./mod.ts", + "tasks": { + "test": "deno run --watch -A mod_test.ts", + "bench": "deno bench --watch -A" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..b42348d --- /dev/null +++ b/deno.lock @@ -0,0 +1,449 @@ +{ + "version": "4", + "specifiers": { + "jsr:@badrap/valita@~0.4.2": "0.4.2", + "jsr:@std/bytes@^1.0.4": "1.0.4", + "jsr:@std/encoding@^1.0.6": "1.0.6", + "jsr:@std/path@^1.0.8": "1.0.8", + "npm:pg-connection-string@^2.7.0": "2.7.0" + }, + "jsr": { + "@badrap/valita@0.4.2": { + "integrity": "af8a829e82eac71adbc7b60352798f94dcc66d19fab16b657957ca9e646c25fd" + }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/encoding@1.0.6": { + "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + } + }, + "npm": { + "pg-connection-string@2.7.0": { + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + } + }, + "redirects": { + "https://deno.land/x/postgres/mod.ts": "https://deno.land/x/postgres@v0.19.3/mod.ts", + "https://deno.land/x/postgresjs/mod.js": "https://deno.land/x/postgresjs@v3.4.5/mod.js" + }, + "remote": { + "https://deno.land/std@0.132.0/_deno_unstable.ts": "23a1a36928f1b6d3b0170aaa67de09af12aa998525f608ff7331b9fb364cbde6", + "https://deno.land/std@0.132.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.132.0/_util/os.ts": "49b92edea1e82ba295ec946de8ffd956ed123e2948d9bd1d3e901b04e4307617", + "https://deno.land/std@0.132.0/_wasm_crypto/crypto.mjs": "3b383eb715e8bfe61b4450ef0644b2653429c88d494807c86c5235979f62e56b", + "https://deno.land/std@0.132.0/_wasm_crypto/crypto.wasm.mjs": "0ad9ecc0d03ca8a083d9109db22e7507f019f63cf55b82ea618ab58855617577", + "https://deno.land/std@0.132.0/_wasm_crypto/mod.ts": "30a93c8b6b6c5b269e96a3e95d2c045d86a496814a8737443b77cad941d6a0b5", + "https://deno.land/std@0.132.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", + "https://deno.land/std@0.132.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", + "https://deno.land/std@0.132.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", + "https://deno.land/std@0.132.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", + "https://deno.land/std@0.132.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", + "https://deno.land/std@0.132.0/async/mod.ts": "2240c6841157738414331f47dee09bb8c0482c5b1980b6e3234dd03515c8132f", + "https://deno.land/std@0.132.0/async/mux_async_iterator.ts": "f4d1d259b0c694d381770ddaaa4b799a94843eba80c17f4a2ec2949168e52d1e", + "https://deno.land/std@0.132.0/async/pool.ts": "97b0dd27c69544e374df857a40902e74e39532f226005543eabacb551e277082", + "https://deno.land/std@0.132.0/async/tee.ts": "1341feb1f5b1a96f8628d0f8fc07d8c43d3813423f18a63bf1b4785568d21b1f", + "https://deno.land/std@0.132.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", + "https://deno.land/std@0.132.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.132.0/bytes/mod.ts": "d3b455c0dbd4804644159d1e25946ade5ee385d2359894de49e2c6101b18b7a9", + "https://deno.land/std@0.132.0/encoding/base64.ts": "c8c16b4adaa60d7a8eee047c73ece26844435e8f7f1328d74593dbb2dd58ea4f", + "https://deno.land/std@0.132.0/encoding/base64url.ts": "55f9d13df02efac10c6f96169daa3e702606a64e8aa27c0295f645f198c27130", + "https://deno.land/std@0.132.0/encoding/hex.ts": "7f023e1e51cfd6b189682e602e8640939e7be71a300a2fcf3daf8f84dc609bbc", + "https://deno.land/std@0.132.0/flags/mod.ts": "430cf2d1c26e00286373b2647ebdca637f7558505e88e9c108a4742cd184c916", + "https://deno.land/std@0.132.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", + "https://deno.land/std@0.132.0/fmt/printf.ts": "e2c0f72146aed1efecf0c39ab928b26ae493a2278f670a871a0fbdcf36ff3379", + "https://deno.land/std@0.132.0/fs/eol.ts": "b92f0b88036de507e7e6fbedbe8f666835ea9dcbf5ac85917fa1fadc919f83a5", + "https://deno.land/std@0.132.0/fs/exists.ts": "cb734d872f8554ea40b8bff77ad33d4143c1187eac621a55bf37781a43c56f6d", + "https://deno.land/std@0.132.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8", + "https://deno.land/std@0.132.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.132.0/node/_buffer.mjs": "f4a7df481d4eed06dc0151b833177d8ef74fc3a96dd4d2b073e690b6ced9474d", + "https://deno.land/std@0.132.0/node/_core.ts": "568d277be2e086af996cbdd599fec569f5280e9a494335ca23ad392b130d7bb9", + "https://deno.land/std@0.132.0/node/_crypto/constants.ts": "49011c87be4e45407ef5e99e96bde3f08656ebd8e6dfc99048c703dd0ce53952", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/base/buffer.js": "73beb8294eb29bd61458bbaaeeb51dfad4ec9c9868a62207a061d908f1637261", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/base/node.js": "4b777980d2a23088698fd2ff065bb311a2c713497d359e674cb6ef6baf267a0f", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/base/reporter.js": "8e4886e8ae311c9a92caf58bbbd8670326ceeae97430f4884e558e4acf8e8598", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/constants/der.js": "354b255479bff22a31d25bf08b217a295071700e37d0991cc05cac9f95e5e7ca", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/decoders/der.js": "c6faf66761daa43fbf79221308443893587c317774047b508a04c570713b76fb", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/decoders/pem.js": "8316ef7ce2ce478bc3dc1e9df1b75225d1eb8fb5d1378f8adf0cf19ecea5b501", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/encoders/der.js": "408336c88d17c5605ea64081261cf42267d8f9fda90098cb560aa6635bb00877", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/encoders/pem.js": "42a00c925b68c0858d6de0ba41ab89935b39fae9117bbf72a9abb2f4b755a2e7", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/mod.js": "7b78859707be10a0a1e4faccdd28cd5a4f71ad74a3e7bebda030757da97cd232", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/bn.js/bn.js": "abd1badd659fd0ae54e6a421a573a25aef4e795edc392178360cf716b144286d", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/aes.js": "1cf4c354c5bb341ffc9ab7207f471229835b021947225bce2e1642f26643847a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/auth_cipher.js": "19b4dbb903e8406eb733176e6318d5e1a3bd382b67b72f7cf8e1c46cc6321ba4", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/decrypter.js": "05c1676942fd8e95837115bc2d1371bcf62e9bf19f6c3348870961fc64ddad0b", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/encrypter.js": "93ec98ab26fbeb5969eae2943e42fb66780f377b9b0ff0ecc32a9ed11201b142", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/ghash.js": "667b64845764a84f0096ef8cf7debed1a5f15ac9af26b379848237be57da399a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/incr32.js": "4a7f0107753e4390b4ccc4dbd5200c5527d43f894f768e131903df30a09dfd67", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/mod.js": "d8eb88e7a317467831473621f32e60d7db9d981f6a2ae45d2fb2af170eab2d22", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cbc.js": "9790799cff181a074686c885708cb8eb473aeb3c86ff2e8d0ff911ae6c1e4431", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cfb.js": "a4e36ede6f26d8559d8f0528a134592761c706145a641bd9ad1100763e831cdb", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cfb1.js": "c6372f4973a68ca742682e81d1165e8869aaabf0091a8b963d4d60e5ee8e6f6a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cfb8.js": "bd29eebb89199b056ff2441f7fb5e0300f458e13dcaaddbb8bc00cbdb199db67", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/ctr.js": "9c2cbac1fc8f9b58334faacb98e6c57e8c3712f673ea4cf2d528a2894998ab2f", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/ecb.js": "9629d193433688f0cfc432eca52838db0fb28d9eb4f45563df952bde50b59763", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/mod.js": "7d8516ef8a20565539eb17cad5bb70add02ac06d1891e8f47cb981c22821787e", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/ofb.js": "c23abaa6f1ec5343e9d7ba61d702acb3d81a0bd3d34dd2004e36975dc043d6ff", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/stream_cipher.js": "a533a03a2214c6b5934ce85a59eb1e04239fd6f429017c7ca3c443ec7e07e68f", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/xor.ts": "4417711c026eb9a07475067cd31fa601e88c2d6ababd606d33d1e74da6fcfd09", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_rsa.js": "de8c98d2379a70d8c239b4886e2b3a11c7204eec39ae6b65d978d0d516ee6b08", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/cipher_base.js": "f565ad9daf3b3dd3b68381bed848da94fb093a9e4e5a48c92f47e26cc229df39", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/evp_bytes_to_key.ts": "8bd9fa445576b3e39586bdbef7c907f1dfda201bf22602d2ca1c6d760366311e", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/asn1.js": "4f33b0197ffbe9cff62e5bad266e6b40d55874ea653552bb32ed251ad091f70a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/certificate.js": "aab306870830a81ad188db8fa8e037d7f5dd6c5abdabbd9739558245d1a12224", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/fix_proc.js": "af3052b76f441878e102ffcfc7420692e65777af765e96f786310ae1acf7f76a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/mod.js": "e923a13b27089a99eeb578d2ffb9b4cfe8ce690918fec05d0024fa126f3e1ce3", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/mgf.js": "5b81dc1680829b564fc5a00b842fb9c88916e4639b4fa27fa8bb6b759d272371", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/mod.js": "eb8b64d7a58ee3823c1b642e799cc7ed1257d99f4d4aefa2b4796dd112ec094a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/private_decrypt.js": "0050df879f7c1338132c45056835f64e32140e2a2d5d03c1366ccce64855f632", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/public_encrypt.js": "0132cb4fb8f72593278474884195b9c52b4e9ba33d8ddd22116d07a07f47005a", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/with_public.js": "7373dac9b53b8331ccf3521c854a131dcb304a2e4d34cd116649118f7919ed0c", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/xor.js": "900c6fc8b95e1861d796193c41988f5f70a09c7059e42887a243d0113ecaf0fd", + "https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/randombytes.ts": "f465cd8e114a3c110297e0143445b12125d729b25bada5bd88d5b30cf612d7dd", + "https://deno.land/std@0.132.0/node/_crypto/hash.ts": "6a84a079412d09ead27b900590f0bede9924bc7ce522b8b7d55183a2aaf63a68", + "https://deno.land/std@0.132.0/node/_crypto/pbkdf2.ts": "00af38578729b3060371dfee70dae502a5848b4cc4787c48f634195cab1ce89a", + "https://deno.land/std@0.132.0/node/_crypto/randomBytes.ts": "04e276bcbfa55b3502c7169deab3f2bf58bbc5e9727634f8a150eff734338e90", + "https://deno.land/std@0.132.0/node/_crypto/randomFill.ts": "019ff2a8330c3ede6e65af28c5a8e3dee9d404975749c8dadf6ba11ccc28528e", + "https://deno.land/std@0.132.0/node/_crypto/randomInt.ts": "2db981c2baf4ddac07b6da71f90677f4acf4dc2d93f351563fdd084d645b8413", + "https://deno.land/std@0.132.0/node/_crypto/scrypt.ts": "caf07a6b8afa6ac582f80f99ed7dc7fefd5476fcd2aad771b8440e5b883d6d70", + "https://deno.land/std@0.132.0/node/_crypto/timingSafeEqual.ts": "4a4ef17e482889d9d82138d5ffc0e787c32c04b1f12b28d076b1a69ceca46af1", + "https://deno.land/std@0.132.0/node/_crypto/types.ts": "d3fae758c5b62f63d8126c76eec31a5559a2f34305defb5fe2a7d9034057ff54", + "https://deno.land/std@0.132.0/node/_dns/_utils.ts": "42494c8b8fa1c13eb134c5696744f77197717fa857e4d05147f2395a739b0a40", + "https://deno.land/std@0.132.0/node/_events.mjs": "d7d56df4b9f69e445064bad5e5558978fb18c18c439bbb62fa13149b40d7fb99", + "https://deno.land/std@0.132.0/node/_fixed_queue.ts": "455b3c484de48e810b13bdf95cd1658ecb1ba6bcb8b9315ffe994efcde3ba5f5", + "https://deno.land/std@0.132.0/node/_fs/_fs_access.ts": "0700488320d37208d000b94767ab37208d469550767edab69b65b09a330f245d", + "https://deno.land/std@0.132.0/node/_fs/_fs_appendFile.ts": "5dca59d7f2ec33316d75da3d6a12d39f5c35b429bddf83f4b2c030b3a289d4b3", + "https://deno.land/std@0.132.0/node/_fs/_fs_chmod.ts": "8fc25677b82a2643686e6f8270a8f1bee87dd60986334591450699e199dac7d5", + "https://deno.land/std@0.132.0/node/_fs/_fs_chown.ts": "57858c54d376648fc3c8cf5a8ad4f7f19fb153b75fac3ed41df0332d757e7de9", + "https://deno.land/std@0.132.0/node/_fs/_fs_close.ts": "785a9d1a6d615e8aa9f5a4ac50c9a131931f8b0e17b3d4671cd1fd25a5c10f2b", + "https://deno.land/std@0.132.0/node/_fs/_fs_common.ts": "6a373d1583d9ec5cc7a8ff1072d77dc999e35282a320b7477038a2b209c304d3", + "https://deno.land/std@0.132.0/node/_fs/_fs_constants.ts": "5c20b190fc6b7cfdaf12a30ba545fc787db2c7bbe87ed5b890da99578116a339", + "https://deno.land/std@0.132.0/node/_fs/_fs_copy.ts": "675eb02a2dfc20dab1186bf6ed0a33b1abae656f440bc0a3ce74f385e0052eef", + "https://deno.land/std@0.132.0/node/_fs/_fs_dir.ts": "8a05f72e32dd568b41ef45f8f55f1f54e9a306a7588475fa7014289cd12872d9", + "https://deno.land/std@0.132.0/node/_fs/_fs_dirent.ts": "649c0a794e7b8d930cdd7e6a168b404aa0448bf784e0cfbe1bd6d25b99052273", + "https://deno.land/std@0.132.0/node/_fs/_fs_exists.ts": "83e9ca6ea1ab3c6c7c3fc45f3c1287ee88839f08140ac11056441537450055bb", + "https://deno.land/std@0.132.0/node/_fs/_fs_fdatasync.ts": "bbd078fea6c62c64d898101d697aefbfbb722797a75e328a82c2a4f2e7eb963d", + "https://deno.land/std@0.132.0/node/_fs/_fs_fstat.ts": "559ff6ff094337db37b0f3108aeaecf42672795af45b206adbd90105afebf9c6", + "https://deno.land/std@0.132.0/node/_fs/_fs_fsync.ts": "590be69ce5363dd4f8867f244cfabe8df89d40f86bbbe44fd00d69411d0b798e", + "https://deno.land/std@0.132.0/node/_fs/_fs_ftruncate.ts": "8eb2a9fcf026bd9b85dc07a22bc452c48db4be05ab83f5f2b6a0549e15c1f75f", + "https://deno.land/std@0.132.0/node/_fs/_fs_futimes.ts": "c753cb9e9f129a11d1110ed43905b8966ac2a1d362ed69d5a34bb44513b00082", + "https://deno.land/std@0.132.0/node/_fs/_fs_link.ts": "3f9ccce31c2e56284fbcf2c65ec2e6fed1d9e67a9997410223486ac5092888e3", + "https://deno.land/std@0.132.0/node/_fs/_fs_lstat.ts": "571cea559d270e3b2e7fc585b0eb051899f6d0e54b1786f5e2cee3e9f71e7f27", + "https://deno.land/std@0.132.0/node/_fs/_fs_mkdir.ts": "68421a23b6d3c2d0142a6d0b3ccdd87903f9c8f98d6754aba554ab4c6b435bb8", + "https://deno.land/std@0.132.0/node/_fs/_fs_mkdtemp.ts": "86eaec96c63ea178c749fa856115a345e9797baecad22297b9ef98e3d62b90e2", + "https://deno.land/std@0.132.0/node/_fs/_fs_open.ts": "b1ca72addd2723b2a5a876378e72609fbe168adad2006f5d7b4f1868beef65ca", + "https://deno.land/std@0.132.0/node/_fs/_fs_read.ts": "3b4ef96aad20f3f29a859125ebeac8c9461574743f70c2a7ef301b8505f7d036", + "https://deno.land/std@0.132.0/node/_fs/_fs_readFile.ts": "3eae6c930e08c1279d400c0f5a008e6d96949ff3a4f5bf7d43e1b94b94ce3854", + "https://deno.land/std@0.132.0/node/_fs/_fs_readdir.ts": "a546f01387b7c49ddc1bd78d0e123a9668c710c56cffb4d9577ef46703cab463", + "https://deno.land/std@0.132.0/node/_fs/_fs_readlink.ts": "00553cd155f3bea565ffe43d7f0c10d75e895455562e1e8ea153e8f4e7ac04c7", + "https://deno.land/std@0.132.0/node/_fs/_fs_realpath.ts": "3ec236e4ad3c171203043422939973b6a948200ec4802425db41fa60c860dde9", + "https://deno.land/std@0.132.0/node/_fs/_fs_rename.ts": "3be71e8f43275c349b7abb9343b6e6764df09fabcbd2d316f8ac170ea556c645", + "https://deno.land/std@0.132.0/node/_fs/_fs_rm.ts": "a9328f99d925d7c74d31361d466ca33475aa7c6d1d6f037a49ce1ed996f0a0b4", + "https://deno.land/std@0.132.0/node/_fs/_fs_rmdir.ts": "b74007891357e709b37e6721eb355a1c4f25575995bb7c961a3c40f03ebc624c", + "https://deno.land/std@0.132.0/node/_fs/_fs_stat.ts": "bd47ce0bfc2b867392abc6ec95878ab4f6dddb94af73903d6fa1a02ba3e26af8", + "https://deno.land/std@0.132.0/node/_fs/_fs_streams.ts": "0e54bd4e41b462a701d6729ea17db01624aa48109e402fea8eecf13be324cf16", + "https://deno.land/std@0.132.0/node/_fs/_fs_symlink.ts": "0bddc37c5092f847634bd41cee0b643b9c03fc541c0e635cf35da1fcb4d0f7fa", + "https://deno.land/std@0.132.0/node/_fs/_fs_truncate.ts": "e2d380f7a81f69c4d4db30c442558ba8d8dea561e5097af41022bb5724e494e5", + "https://deno.land/std@0.132.0/node/_fs/_fs_unlink.ts": "c537ca98e507972d65f0b113a179b5f5083f0da3e6f9fae29895fd2a9660c18a", + "https://deno.land/std@0.132.0/node/_fs/_fs_utimes.ts": "c4446b7e39bf6977eca4364360501a97b96db9ea41e0cdf49abddab73481a175", + "https://deno.land/std@0.132.0/node/_fs/_fs_watch.ts": "2338de777458021d39cb9f0a5f3ea1bd9109a7ca2c2ad6ec41029df1753838f8", + "https://deno.land/std@0.132.0/node/_fs/_fs_write.mjs": "8c130b8b9522e1e4b08e687eb27939240260c115fda1e38e99c57b4f3af6481f", + "https://deno.land/std@0.132.0/node/_fs/_fs_writeFile.ts": "79d176021c8ceae0d956763a33834166ebc3f1691ed9219a21674b2374f115c3", + "https://deno.land/std@0.132.0/node/_fs/_fs_writev.mjs": "274df0a109010862c8f8b320dc7784de9bd9425fe2a6afd05f1f06f547a25cba", + "https://deno.land/std@0.132.0/node/_next_tick.ts": "64c361f6bca21df2a72dd77b84bd49d80d97a694dd3080703bc78f52146351d1", + "https://deno.land/std@0.132.0/node/_options.ts": "27f3c1269a700d330cc046cf748aa9178b8fc39d1473de625688e07cb0eb9d28", + "https://deno.land/std@0.132.0/node/_process/exiting.ts": "bc9694769139ffc596f962087155a8bfef10101d03423b9dcbc51ce6e1f88fce", + "https://deno.land/std@0.132.0/node/_process/process.ts": "84644b184053835670f79652d1ce3312c9ad079c211e6207ebefeedf159352a3", + "https://deno.land/std@0.132.0/node/_process/stdio.mjs": "971c3b086040d8521562155db13f22f9971d5c42c852b2081d4d2f0d8b6ab6bd", + "https://deno.land/std@0.132.0/node/_process/streams.mjs": "555062e177ad05f887147651fdda25fa55098475fcf142c8d162b8fe14097bbb", + "https://deno.land/std@0.132.0/node/_stream.mjs": "07f6cbabaad0382fb4b9a25e70ac3093a44022b859247f64726746e6373f1c91", + "https://deno.land/std@0.132.0/node/_util/_util_callbackify.ts": "79928ad80df3e469f7dcdb198118a7436d18a9f6c08bd7a4382332ad25a718cf", + "https://deno.land/std@0.132.0/node/_utils.ts": "c2c352e83c4c96f5ff994b1c8246bff2abcb21bfc3f1c06162cb3af1d201e615", + "https://deno.land/std@0.132.0/node/buffer.ts": "fbecbf3f237fa49bec96e97ecf56a7b92d48037b3d11219288e68943cc921600", + "https://deno.land/std@0.132.0/node/crypto.ts": "fffbc3fc3dcc16ea986d3e89eed5f70db7dfef2c18d1205a8c8fe5327ee0192d", + "https://deno.land/std@0.132.0/node/dns.ts": "ae2abd1bc8ac79543fe4d702f2aa3607101dc788b6eeba06e06436cb42ee3779", + "https://deno.land/std@0.132.0/node/events.ts": "a1d40fc0dbccc944379ef968b80ea08f9fce579e88b5057fdb64e4f0812476dd", + "https://deno.land/std@0.132.0/node/fs.ts": "21a3189c460bd37ac3f6734e040587125b7c8435c0a9da4e6c57544a3aca81c2", + "https://deno.land/std@0.132.0/node/internal/assert.mjs": "118327c8866266534b30d3a36ad978204af7336dc2db3158b8167192918d4e06", + "https://deno.land/std@0.132.0/node/internal/async_hooks.ts": "8eca5b80f58ffb259e9b3a73536dc2fe2e67d07fd24bfe2aee325a4aa435edb3", + "https://deno.land/std@0.132.0/node/internal/blob.mjs": "52080b2f40b114203df67f8a6650f9fe3c653912b8b3ef2f31f029853df4db53", + "https://deno.land/std@0.132.0/node/internal/buffer.mjs": "6662fe7fe517329453545be34cea27a24f8ccd6d09afd4f609f11ade2b6dfca7", + "https://deno.land/std@0.132.0/node/internal/crypto/keys.ts": "16ce7b15a9fc5e4e3dee8fde75dae12f3d722558d5a1a6e65a9b4f86d64a21e9", + "https://deno.land/std@0.132.0/node/internal/crypto/util.mjs": "1de55a47fdbed6721b467a77ba48fdd1550c10b5eee77bbdb602eaffee365a5e", + "https://deno.land/std@0.132.0/node/internal/dtrace.ts": "50dd0e77b0269e47ff673bdb9ad0ef0ea3a3c53ac30c1695883ce4748e04ca14", + "https://deno.land/std@0.132.0/node/internal/error_codes.ts": "ac03c4eae33de3a69d6c98e8678003207eecf75a6900eb847e3fea3c8c9e6d8f", + "https://deno.land/std@0.132.0/node/internal/errors.ts": "25f91691225b001660e6e64745ecd336fbf562cf0185e8896ff013c2d0226794", + "https://deno.land/std@0.132.0/node/internal/fs/streams.ts": "c925db185efdf56c35cde8270c07d61698b80603a90e07caf1cb4ff80abf195b", + "https://deno.land/std@0.132.0/node/internal/fs/utils.mjs": "2a571ecbd169b444f07b7193306f108fdcb4bfd9b394b33716ad05edf30e899e", + "https://deno.land/std@0.132.0/node/internal/hide_stack_frames.ts": "a91962ec84610bc7ec86022c4593cdf688156a5910c07b5bcd71994225c13a03", + "https://deno.land/std@0.132.0/node/internal/idna.ts": "a8bdd28431f06630d8aad85d3cb8fd862459107af228c8805373ad2080f1c587", + "https://deno.land/std@0.132.0/node/internal/net.ts": "1239886cd2508a68624c2dae8abf895e8aa3bb15a748955349f9ac5539032238", + "https://deno.land/std@0.132.0/node/internal/normalize_encoding.mjs": "3779ec8a7adf5d963b0224f9b85d1bc974a2ec2db0e858396b5d3c2c92138a0a", + "https://deno.land/std@0.132.0/node/internal/process/per_thread.mjs": "a42b1dcfb009ad5039144a999a35a429e76112f9322febbe353eda9d1879d936", + "https://deno.land/std@0.132.0/node/internal/querystring.ts": "c3b23674a379f696e505606ddce9c6feabe9fc497b280c56705c340f4028fe74", + "https://deno.land/std@0.132.0/node/internal/stream_base_commons.ts": "934a9e69f46f2de644956edfa9fb040af7861e326fe5325dab38ef9caf2940bc", + "https://deno.land/std@0.132.0/node/internal/streams/_utils.ts": "77fceaa766679847e4d4c3c96b2573c00a790298d90551e8e4df1d5e0fdaad3b", + "https://deno.land/std@0.132.0/node/internal/streams/add-abort-signal.mjs": "5623b83fa64d439cc4a1f09ae47ec1db29512cc03479389614d8f62a37902f5e", + "https://deno.land/std@0.132.0/node/internal/streams/buffer_list.mjs": "c6a7b29204fae025ff5e9383332acaea5d44bc7c522a407a79b8f7a6bc6c312d", + "https://deno.land/std@0.132.0/node/internal/streams/compose.mjs": "b522daab35a80ae62296012a4254fd7edfc0366080ffe63ddda4e38fe6b6803e", + "https://deno.land/std@0.132.0/node/internal/streams/destroy.mjs": "9c9bbeb172a437041d529829f433df72cf0b63ae49f3ee6080a55ffbef7572ad", + "https://deno.land/std@0.132.0/node/internal/streams/duplex.mjs": "b014087cd04f79b8a4028d8b9423b987e07bbfacf3b5df518cb752ac3657580f", + "https://deno.land/std@0.132.0/node/internal/streams/end-of-stream.mjs": "38be76eaceac231dfde643e72bc0940625446bf6d1dbd995c91c5ba9fd59b338", + "https://deno.land/std@0.132.0/node/internal/streams/from.mjs": "134255c698ed63b33199911eb8e042f8f67e9682409bb11552e6120041ed1872", + "https://deno.land/std@0.132.0/node/internal/streams/legacy.mjs": "6ea28db95d4503447473e62f0b23ff473bfe1751223c33a3c5816652e93b257a", + "https://deno.land/std@0.132.0/node/internal/streams/passthrough.mjs": "a51074193b959f3103d94de41e23a78dfcff532bdba53af9146b86340d85eded", + "https://deno.land/std@0.132.0/node/internal/streams/pipeline.mjs": "9890b121759ede869174ef70c011fde964ca94d81f2ed97b8622d7cb17b49285", + "https://deno.land/std@0.132.0/node/internal/streams/readable.mjs": "a70c41171ae25c556b52785b0c178328014bd33d8c0e4d229d4adaac7414b6ca", + "https://deno.land/std@0.132.0/node/internal/streams/state.mjs": "9ef917392a9d8005a6e038260c5fd31518d2753aea0bc9e39824c199310434cb", + "https://deno.land/std@0.132.0/node/internal/streams/transform.mjs": "3b361abad2ac78f7ccb6f305012bafdc0e983dfa4bb6ecddb4626e34a781a5f5", + "https://deno.land/std@0.132.0/node/internal/streams/utils.mjs": "06c21d0db0d51f1bf1e3225a661c3c29909be80355d268e64ee5922fc5eb6c5e", + "https://deno.land/std@0.132.0/node/internal/streams/writable.mjs": "ad4e2b176ffdf752c8e678ead3a514679a5a8d652f4acf797115dceb798744d5", + "https://deno.land/std@0.132.0/node/internal/timers.mjs": "b43e24580cec2dd50f795e4342251a79515c0db21630c25b40fdc380a78b74e7", + "https://deno.land/std@0.132.0/node/internal/url.ts": "eacef0ace4f4c5394e9818a81499f4871b2a993d1bd3b902047e44a381ef0e22", + "https://deno.land/std@0.132.0/node/internal/util.mjs": "2f0c8ff553c175ea6e4ed13d7cd7cd6b86dc093dc2f783c6c3dfc63f60a0943e", + "https://deno.land/std@0.132.0/node/internal/util/comparisons.ts": "680b55fe8bdf1613633bc469fa0440f43162c76dbe36af9aa2966310e1bb9f6e", + "https://deno.land/std@0.132.0/node/internal/util/debuglog.ts": "99e91bdf26f6c67861031f684817e1705a5bc300e81346585b396f413387edfb", + "https://deno.land/std@0.132.0/node/internal/util/inspect.mjs": "d1c2569c66a3dab45eec03208f22ad4351482527859c0011a28a6c797288a0aa", + "https://deno.land/std@0.132.0/node/internal/util/types.ts": "b2dacb8f1f5d28a51c4da5c5b75172b7fcf694073ce95ca141323657e18b0c60", + "https://deno.land/std@0.132.0/node/internal/validators.mjs": "a7e82eafb7deb85c332d5f8d9ffef052f46a42d4a121eada4a54232451acc49a", + "https://deno.land/std@0.132.0/node/internal_binding/_libuv_winerror.ts": "801e05c2742ae6cd42a5f0fd555a255a7308a65732551e962e5345f55eedc519", + "https://deno.land/std@0.132.0/node/internal_binding/_node.ts": "e4075ba8a37aef4eb5b592c8e3807c39cb49ca8653faf8e01a43421938076c1b", + "https://deno.land/std@0.132.0/node/internal_binding/_utils.ts": "1c50883b5751a9ea1b38951e62ed63bacfdc9d69ea665292edfa28e1b1c5bd94", + "https://deno.land/std@0.132.0/node/internal_binding/_winerror.ts": "8811d4be66f918c165370b619259c1f35e8c3e458b8539db64c704fbde0a7cd2", + "https://deno.land/std@0.132.0/node/internal_binding/async_wrap.ts": "f06b8a403ad871248eb064190d27bf6fefdbe948991e71a18d7077390d5773f9", + "https://deno.land/std@0.132.0/node/internal_binding/buffer.ts": "722c62b85f966e0777b2d98c021b60e75d7f2c2dabc43413ef37d60dbd13a5d9", + "https://deno.land/std@0.132.0/node/internal_binding/cares_wrap.ts": "25b7b5d56612b2985260b673021828d6511a1c83b4c1927f5732cad2f2a718af", + "https://deno.land/std@0.132.0/node/internal_binding/config.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/connection_wrap.ts": "0380444ee94d5bd7b0b09921223d16729c9762a94e80b7f51eda49c7f42e6d0a", + "https://deno.land/std@0.132.0/node/internal_binding/constants.ts": "aff06aac49eda4234bd3a2b0b8e1fbfc67824e281c532ff9960831ab503014cc", + "https://deno.land/std@0.132.0/node/internal_binding/contextify.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/credentials.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/crypto.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/errors.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/fs.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/fs_dir.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/fs_event_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/handle_wrap.ts": "e59df84b1fb1b9823b09774b3e512d9c0029b4557400d09dd02cd7661c2c4830", + "https://deno.land/std@0.132.0/node/internal_binding/heap_utils.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/http_parser.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/icu.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/inspector.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/js_stream.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/messaging.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/mod.ts": "f68e74e8eed84eaa6b0de24f0f4c47735ed46866d7ee1c5a5e7c0667b4f0540f", + "https://deno.land/std@0.132.0/node/internal_binding/module_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/native_module.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/natives.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/node_file.ts": "c96ee0b2af319a3916de950a6c4b0d5fb00d09395c51cd239c54d95d62567aaf", + "https://deno.land/std@0.132.0/node/internal_binding/node_options.ts": "3cd5706153d28a4f5944b8b162c1c61b7b8e368a448fb1a2cff9f7957d3db360", + "https://deno.land/std@0.132.0/node/internal_binding/options.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/os.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/performance.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/pipe_wrap.ts": "00e942327f8e1c4b74a5888a82f0e16ba775cd09af804f96b6f6849b7eab1719", + "https://deno.land/std@0.132.0/node/internal_binding/process_methods.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/report.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/serdes.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/signal_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/spawn_sync.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/stream_wrap.ts": "d6e96f4b89d82ad5cc9b243c3d3880c9d85086165da54a7d85821a63491e5abf", + "https://deno.land/std@0.132.0/node/internal_binding/string_decoder.ts": "5cb1863763d1e9b458bc21d6f976f16d9c18b3b3f57eaf0ade120aee38fba227", + "https://deno.land/std@0.132.0/node/internal_binding/symbols.ts": "51cfca9bb6132d42071d4e9e6b68a340a7f274041cfcba3ad02900886e972a6c", + "https://deno.land/std@0.132.0/node/internal_binding/task_queue.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/tcp_wrap.ts": "10c64d5e092a8bff99cfe05adea716e4e52f4158662a5821790953e47e2cc21c", + "https://deno.land/std@0.132.0/node/internal_binding/timers.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/tls_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/trace_events.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/tty_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/types.ts": "4c26fb74ba2e45de553c15014c916df6789529a93171e450d5afb016b4c765e7", + "https://deno.land/std@0.132.0/node/internal_binding/udp_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/url.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/util.ts": "90364292e2bd598ab5d105b48ca49817b6708f2d1d9cbaf08b2b3ab5ca4c90a7", + "https://deno.land/std@0.132.0/node/internal_binding/uv.ts": "3821bc5e676d6955d68f581988c961d77dd28190aba5a9c59f16001a4deb34ba", + "https://deno.land/std@0.132.0/node/internal_binding/v8.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/worker.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/internal_binding/zlib.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.132.0/node/net.ts": "dfcb7e412abb3d5c55edde7d823b0ccb9601f7d40555ae3c862810c78b176185", + "https://deno.land/std@0.132.0/node/os.ts": "943d3294a7a00f39491148cd2097cdbf101233a421262223bb20ae702c059df5", + "https://deno.land/std@0.132.0/node/path.ts": "c65858e9cbb52dbc0dd348eefcdc41e82906c39cfa7982f2d4d805e828414b8c", + "https://deno.land/std@0.132.0/node/path/_constants.ts": "bd26f24a052b7d6b746151f4a236d29ab3c2096883bb6449c2fa499494406672", + "https://deno.land/std@0.132.0/node/path/_interface.ts": "6034ee29f6f295460ec82db1a94df9269aecbb0eceb81be72e9d843f8e8a97e6", + "https://deno.land/std@0.132.0/node/path/_util.ts": "9d4735fc05f8f1fb94406450e84e23fd201dc3fef5298b009e44cfa4e797b8f0", + "https://deno.land/std@0.132.0/node/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4", + "https://deno.land/std@0.132.0/node/path/glob.ts": "d6b64a24f148855a6e8057a171a2f9910c39e492e4ccec482005205b28eb4533", + "https://deno.land/std@0.132.0/node/path/mod.ts": "62e21dc6e1fe2e9742fce85de631a7b067d968544fe66954578e6d73c97369a2", + "https://deno.land/std@0.132.0/node/path/posix.ts": "9dd5fc83c4ae0e0b700bef43c88c67e276840c187a66d4d6a661440cf1fecc52", + "https://deno.land/std@0.132.0/node/path/separator.ts": "c908c9c28ebe7f1fea67daaccf84b63af90d882fe986f9fa03af9563a852723a", + "https://deno.land/std@0.132.0/node/path/win32.ts": "f869ee449b6dee69b13e2d1f8f7f1d01c7ae1e67fa573eab789429929f7a3864", + "https://deno.land/std@0.132.0/node/process.ts": "699f47f2f177556e17e2f7d0dcd3705ff5065cbdf72029e534c1540404d6f501", + "https://deno.land/std@0.132.0/node/querystring.ts": "967b8a7b00a73ebe373666deb3a7e501f164bac27bb342fde7221ecbb3522689", + "https://deno.land/std@0.132.0/node/stream.ts": "d127faa074a9e3886e4a01dcfe9f9a6a4b5641f76f6acc356e8ded7da5dc2c81", + "https://deno.land/std@0.132.0/node/stream/promises.mjs": "b263c09f2d6bd715dc514fab3f99cca84f442e2d23e87adbe76e32ea46fc87e6", + "https://deno.land/std@0.132.0/node/string_decoder.ts": "51ce85a173d2e36ac580d418bb48b804adb41732fc8bd85f7d5d27b7accbc61f", + "https://deno.land/std@0.132.0/node/timers.ts": "2d66fcd21e37acf76c3a699a97230da57cc21382c8e885b3c5377b37efd0f06c", + "https://deno.land/std@0.132.0/node/url.ts": "bc0bde2774854b6a377c4c61fa73e5a28283cbeb7f8703479f44e471219c33a8", + "https://deno.land/std@0.132.0/node/util.ts": "7fd6933b37af89a8e64d73dc6ee1732455a59e7e6d0965311fbd73cd634ea630", + "https://deno.land/std@0.132.0/node/util/types.mjs": "f9288198cacd374b41bae7e92a23179d3160f4c0eaf14e19be3a4e7057219a60", + "https://deno.land/std@0.132.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.132.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.132.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.132.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.132.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.132.0/path/mod.ts": "4275129bb766f0e475ecc5246aa35689eeade419d72a48355203f31802640be7", + "https://deno.land/std@0.132.0/path/posix.ts": "663e4a6fe30a145f56aa41a22d95114c4c5582d8b57d2d7c9ed27ad2c47636bb", + "https://deno.land/std@0.132.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.132.0/path/win32.ts": "e7bdf63e8d9982b4d8a01ef5689425c93310ece950e517476e22af10f41a136e", + "https://deno.land/std@0.132.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", + "https://deno.land/std@0.132.0/testing/_diff.ts": "9d849cd6877694152e01775b2d93f9d6b7aef7e24bfe3bfafc4d7a1ac8e9f392", + "https://deno.land/std@0.132.0/testing/asserts.ts": "b0ef969032882b1f7eb1c7571e313214baa1485f7b61cf35807b2434e254365c", + "https://deno.land/std@0.214.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.214.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.214.0/async/delay.ts": "8e1d18fe8b28ff95885e2bc54eccec1713f57f756053576d8228e6ca110793ad", + "https://deno.land/std@0.214.0/bytes/copy.ts": "f29c03168853720dfe82eaa57793d0b9e3543ebfe5306684182f0f1e3bfd422a", + "https://deno.land/std@0.214.0/crypto/_fnv/fnv32.ts": "ba2c5ef976b9f047d7ce2d33dfe18671afc75154bcf20ef89d932b2fe8820535", + "https://deno.land/std@0.214.0/crypto/_fnv/fnv64.ts": "580cadfe2ff333fe253d15df450f927c8ac7e408b704547be26aab41b5772558", + "https://deno.land/std@0.214.0/crypto/_fnv/mod.ts": "8dbb60f062a6e77b82f7a62ac11fabfba52c3cd408c21916b130d8f57a880f96", + "https://deno.land/std@0.214.0/crypto/_fnv/util.ts": "27b36ce3440d0a180af6bf1cfc2c326f68823288540a354dc1d636b781b9b75f", + "https://deno.land/std@0.214.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "76c727912539737def4549bb62a96897f37eb334b979f49c57b8af7a1617635e", + "https://deno.land/std@0.214.0/crypto/_wasm/mod.ts": "c55f91473846827f077dfd7e5fc6e2726dee5003b6a5747610707cdc638a22ba", + "https://deno.land/std@0.214.0/crypto/crypto.ts": "4448f8461c797adba8d70a2c60f7795a546d7a0926e96366391bffdd06491c16", + "https://deno.land/std@0.214.0/datetime/_common.ts": "a62214c1924766e008e27d3d843ceba4b545dc2aa9880de0ecdef9966d5736b6", + "https://deno.land/std@0.214.0/datetime/parse.ts": "bb248bbcb3cd54bcaf504a1ee670fc4695e429d9019c06af954bbe2bcb8f1d02", + "https://deno.land/std@0.214.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", + "https://deno.land/std@0.214.0/encoding/base64.ts": "96e61a556d933201266fea84ae500453293f2aff130057b579baafda096a96bc", + "https://deno.land/std@0.214.0/encoding/hex.ts": "4d47d3b25103cf81a2ed38f54b394d39a77b63338e1eaa04b70c614cb45ec2e6", + "https://deno.land/std@0.214.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb", + "https://deno.land/std@0.214.0/io/buf_reader.ts": "c73aad99491ee6db3d6b001fa4a780e9245c67b9296f5bad9c0fa7384e35d47a", + "https://deno.land/std@0.214.0/io/buf_writer.ts": "f82f640c8b3a820f600a8da429ad0537037c7d6a78426bbca2396fb1f75d3ef4", + "https://deno.land/std@0.214.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", + "https://deno.land/std@0.214.0/path/_common/assert_path.ts": "2ca275f36ac1788b2acb60fb2b79cb06027198bc2ba6fb7e163efaedde98c297", + "https://deno.land/std@0.214.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.214.0/path/_common/common.ts": "6157c7ec1f4db2b4a9a187efd6ce76dcaf1e61cfd49f87e40d4ea102818df031", + "https://deno.land/std@0.214.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.214.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.214.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.214.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.214.0/path/_common/glob_to_reg_exp.ts": "2007aa87bed6eb2c8ae8381adcc3125027543d9ec347713c1ad2c68427330770", + "https://deno.land/std@0.214.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.214.0/path/_common/normalize_string.ts": "dfdf657a1b1a7db7999f7c575ee7e6b0551d9c20f19486c6c3f5ff428384c965", + "https://deno.land/std@0.214.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.214.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.214.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.214.0/path/_interface.ts": "a1419fcf45c0ceb8acdccc94394e3e94f99e18cfd32d509aab514c8841799600", + "https://deno.land/std@0.214.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.214.0/path/basename.ts": "5d341aadb7ada266e2280561692c165771d071c98746fcb66da928870cd47668", + "https://deno.land/std@0.214.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.214.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.214.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.214.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.214.0/path/format.ts": "98fad25f1af7b96a48efb5b67378fcc8ed77be895df8b9c733b86411632162af", + "https://deno.land/std@0.214.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.214.0/path/glob_to_regexp.ts": "83c5fd36a8c86f5e72df9d0f45317f9546afa2ce39acaafe079d43a865aced08", + "https://deno.land/std@0.214.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.214.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.214.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.214.0/path/join_globs.ts": "e9589869a33dc3982101898ee50903db918ca00ad2614dbe3934d597d7b1fbea", + "https://deno.land/std@0.214.0/path/mod.ts": "ffeaccb713dbe6c72e015b7c767f753f8ec5fbc3b621ff5eeee486ffc2c0ddda", + "https://deno.land/std@0.214.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.214.0/path/normalize_glob.ts": "98ee8268fad271193603271c203ae973280b5abfbdd2cbca1053fd2af71869ca", + "https://deno.land/std@0.214.0/path/parse.ts": "65e8e285f1a63b714e19ef24b68f56e76934c3df0b6e65fd440d3991f4f8aefb", + "https://deno.land/std@0.214.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.214.0/path/posix/basename.ts": "39ee27a29f1f35935d3603ccf01d53f3d6e0c5d4d0f84421e65bd1afeff42843", + "https://deno.land/std@0.214.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.214.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.214.0/path/posix/dirname.ts": "6535d2bdd566118963537b9dda8867ba9e2a361015540dc91f5afbb65c0cce8b", + "https://deno.land/std@0.214.0/path/posix/extname.ts": "8d36ae0082063c5e1191639699e6f77d3acf501600a3d87b74943f0ae5327427", + "https://deno.land/std@0.214.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.214.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.214.0/path/posix/glob_to_regexp.ts": "54d3ff40f309e3732ab6e5b19d7111d2d415248bcd35b67a99defcbc1972e697", + "https://deno.land/std@0.214.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.214.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.214.0/path/posix/join.ts": "aef88d5fa3650f7516730865dbb951594d1a955b785e2450dbee93b8e32694f3", + "https://deno.land/std@0.214.0/path/posix/join_globs.ts": "ee2f4676c5b8a0dfa519da58b8ade4d1c4aa8dd3fe35619edec883ae9df1f8c9", + "https://deno.land/std@0.214.0/path/posix/mod.ts": "563a18c2b3ddc62f3e4a324ff0f583e819b8602a72ad880cb98c9e2e34f8db5b", + "https://deno.land/std@0.214.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.214.0/path/posix/normalize_glob.ts": "65f0138fa518ef9ece354f32889783fc38cdf985fb02dcf1c3b14fa47d665640", + "https://deno.land/std@0.214.0/path/posix/parse.ts": "d5bac4eb21262ab168eead7e2196cb862940c84cee572eafedd12a0d34adc8fb", + "https://deno.land/std@0.214.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.214.0/path/posix/resolve.ts": "bac20d9921beebbbb2b73706683b518b1d0c1b1da514140cee409e90d6b2913a", + "https://deno.land/std@0.214.0/path/posix/separator.ts": "c9ecae5c843170118156ac5d12dc53e9caf6a1a4c96fc8b1a0ab02dff5c847b0", + "https://deno.land/std@0.214.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.214.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.214.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.214.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.214.0/path/separator.ts": "c6c890507f944a1f5cb7d53b8d638d6ce3cf0f34609c8d84a10c1eaa400b77a9", + "https://deno.land/std@0.214.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.214.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.214.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.214.0/path/windows/basename.ts": "e2dbf31d1d6385bfab1ce38c333aa290b6d7ae9e0ecb8234a654e583cf22f8fe", + "https://deno.land/std@0.214.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.214.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.214.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.214.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.214.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.214.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.214.0/path/windows/glob_to_regexp.ts": "6dcd1242bd8907aa9660cbdd7c93446e6927b201112b0cba37ca5d80f81be51b", + "https://deno.land/std@0.214.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.214.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.214.0/path/windows/join.ts": "e0b3356615c1a75c56ebb6a7311157911659e11fd533d80d724800126b761ac3", + "https://deno.land/std@0.214.0/path/windows/join_globs.ts": "ee2f4676c5b8a0dfa519da58b8ade4d1c4aa8dd3fe35619edec883ae9df1f8c9", + "https://deno.land/std@0.214.0/path/windows/mod.ts": "7d6062927bda47c47847ffb55d8f1a37b0383840aee5c7dfc93984005819689c", + "https://deno.land/std@0.214.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.214.0/path/windows/normalize_glob.ts": "179c86ba89f4d3fe283d2addbe0607341f79ee9b1ae663abcfb3439db2e97810", + "https://deno.land/std@0.214.0/path/windows/parse.ts": "b9239edd892a06a06625c1b58425e199f018ce5649ace024d144495c984da734", + "https://deno.land/std@0.214.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.214.0/path/windows/resolve.ts": "75b2e3e1238d840782cee3d8864d82bfaa593c7af8b22f19c6422cf82f330ab3", + "https://deno.land/std@0.214.0/path/windows/separator.ts": "e51c5522140eff4f8402617c5c68a201fdfa3a1a8b28dc23587cff931b665e43", + "https://deno.land/std@0.214.0/path/windows/to_file_url.ts": "1cd63fd35ec8d1370feaa4752eccc4cc05ea5362a878be8dc7db733650995484", + "https://deno.land/std@0.214.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", + "https://deno.land/x/postgres@v0.19.3/client.ts": "d141c65c20484c545a1119c9af7a52dcc24f75c1a5633de2b9617b0f4b2ed5c1", + "https://deno.land/x/postgres@v0.19.3/client/error.ts": "05b0e35d65caf0ba21f7f6fab28c0811da83cd8b4897995a2f411c2c83391036", + "https://deno.land/x/postgres@v0.19.3/connection/auth.ts": "db15c1659742ef4d2791b32834950278dc7a40cb931f8e434e6569298e58df51", + "https://deno.land/x/postgres@v0.19.3/connection/connection.ts": "198a0ecf92a0d2aa72db3bb88b8f412d3b1f6b87d464d5f7bff9aa3b6aff8370", + "https://deno.land/x/postgres@v0.19.3/connection/connection_params.ts": "463d7a9ed559f537a55d6928cab62e1c31b808d08cd0411b6ae461d0c0183c93", + "https://deno.land/x/postgres@v0.19.3/connection/message.ts": "20da5d80fc4d7ddb7b850083e0b3fa8734eb26642221dad89c62e27d78e57a4d", + "https://deno.land/x/postgres@v0.19.3/connection/message_code.ts": "12bcb110df6945152f9f6c63128786558d7ad1e61006920daaa16ef85b3bab7d", + "https://deno.land/x/postgres@v0.19.3/connection/packet.ts": "050aeff1fc13c9349e89451a155ffcd0b1343dc313a51f84439e3e45f64b56c8", + "https://deno.land/x/postgres@v0.19.3/connection/scram.ts": "532d4d58b565a2ab48fb5e1e14dc9bfb3bb283d535011e371e698eb4a89dd994", + "https://deno.land/x/postgres@v0.19.3/debug.ts": "8add17699191f11e6830b8c95d9de25857d221bb2cf6c4ae22254d395895c1f9", + "https://deno.land/x/postgres@v0.19.3/deps.ts": "c312038fe64b8368f8a294119f11d8f235fe67de84d7c3b0ef67b3a56628171a", + "https://deno.land/x/postgres@v0.19.3/mod.ts": "4930c7b44f8d16ea71026f7e3ef22a2322d84655edceacd55f7461a9218d8560", + "https://deno.land/x/postgres@v0.19.3/pool.ts": "2289f029e7a3bd3d460d4faa71399a920b7406c92a97c0715d6e31dbf1380ec3", + "https://deno.land/x/postgres@v0.19.3/query/array_parser.ts": "ff72d3e026e3022a1a223a6530be5663f8ebbd911ed978291314e7fe6c2f2464", + "https://deno.land/x/postgres@v0.19.3/query/decode.ts": "3e89ad2a662eab66a4f4e195ff0924d71d199af3c2f5637d1ae650301a03fa9b", + "https://deno.land/x/postgres@v0.19.3/query/decoders.ts": "6a73da1024086ab91e233648c850dccbde59248b90d87054bbbd7f0bf4a50681", + "https://deno.land/x/postgres@v0.19.3/query/encode.ts": "5b1c305bc7352a6f9fe37f235dddfc23e26419c77a133b4eaea42cf136481aa6", + "https://deno.land/x/postgres@v0.19.3/query/oid.ts": "21fc714ac212350ba7df496f88ea9e01a4ee0458911d0f2b6a81498e12e7af4c", + "https://deno.land/x/postgres@v0.19.3/query/query.ts": "510f9a27da87ed7b31b5cbcd14bf3028b441ac2ddc368483679d0b86a9d9f213", + "https://deno.land/x/postgres@v0.19.3/query/transaction.ts": "8f4eef68f8e9b4be216199404315e6e08fe1fe98afb2e640bffd077662f79678", + "https://deno.land/x/postgres@v0.19.3/query/types.ts": "540f6f973d493d63f2c0059a09f3368071f57931bba68bea408a635a3e0565d6", + "https://deno.land/x/postgres@v0.19.3/utils/deferred.ts": "5420531adb6c3ea29ca8aac57b9b59bd3e4b9a938a4996bbd0947a858f611080", + "https://deno.land/x/postgres@v0.19.3/utils/utils.ts": "ca47193ea03ff5b585e487a06f106d367e509263a960b787197ce0c03113a738", + "https://deno.land/x/postgresjs@v3.4.5/mod.js": "cb68f17d6d90df318934deccdb469d740be0888e7a597a9e7eea7100ce36a252", + "https://deno.land/x/postgresjs@v3.4.5/polyfills.js": "318eb01f2b4cc33a46c59f3ddc11f22a56d6b1db8b7719b2ad7decee63a5bd47", + "https://deno.land/x/postgresjs@v3.4.5/src/bytes.js": "f2de43bdc8fa5dc4b169f2c70d5d8b053a3dea8f85ef011d7b27dec69e14ebb7", + "https://deno.land/x/postgresjs@v3.4.5/src/connection.js": "e63062451fb6a7284c14540b3f268d1373c3028fb0f3b234056ad56569190e8f", + "https://deno.land/x/postgresjs@v3.4.5/src/errors.js": "85cfbed9a5ab0db41ab8e97b806c881af29807dfe99bc656fdf1a18c1c13b6c6", + "https://deno.land/x/postgresjs@v3.4.5/src/index.js": "9dca008e765675f8218d4e2e3ccc75359cc2240f7be4e80bf6735e92b5562e3a", + "https://deno.land/x/postgresjs@v3.4.5/src/large.js": "f3e770cdb7cc695f7b50687b4c6c4b7252129515486ec8def98b7582ee7c54ef", + "https://deno.land/x/postgresjs@v3.4.5/src/query.js": "67c45a5151032aa46b587abc15381fe4efd97c696e5c1b53082b8161309c4ee2", + "https://deno.land/x/postgresjs@v3.4.5/src/queue.js": "15e6345adb6708bf3b99ad39fc2231c2fb61de5f6cba4b7a7a6be881482a4ec3", + "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" + } +} diff --git a/lstd.ts b/lstd.ts new file mode 100644 index 0000000..b3f74f0 --- /dev/null +++ b/lstd.ts @@ -0,0 +1 @@ +export * from "https://git.lua.re/luaneko/lstd/raw/tag/0.1.3/mod.ts"; diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..a6ebf31 --- /dev/null +++ b/mod.ts @@ -0,0 +1,126 @@ +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"; + +export { + WireError, + PostgresError, + type LogLevel, + type Transaction, + type Channel, + type Parameters, +} from "./wire.ts"; +export { + type SqlFragment, + type FromSql, + type ToSql, + SqlValue, + sql, + is_sql, +} from "./sql.ts"; +export { + Query, + type Row, + type CommandResult, + type Result, + type Results, + type ResultStream, +} 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; + from_sql?: FromSql; + to_sql?: ToSql; +}; + +type ParsedOptions = Infer; +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), +}); + +function parse_opts(s: string, options: Options) { + const { + host, + port, + user, + password, + database, + ssl: _ssl, // TODO: + ...runtime_params + } = pg_conn_string.parse(s); + + const { PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, USER } = + Deno.env.toObject(); + + return ParsedOptions.parse({ + ...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, + 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)); + } +} diff --git a/mod_test.ts b/mod_test.ts new file mode 100644 index 0000000..fbecb5f --- /dev/null +++ b/mod_test.ts @@ -0,0 +1,13 @@ +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`); + }); +}); diff --git a/query.ts b/query.ts new file mode 100644 index 0000000..367b081 --- /dev/null +++ b/query.ts @@ -0,0 +1,217 @@ +import type { ObjectType } from "./valita.ts"; +import { from_utf8, jit, to_utf8 } from "./lstd.ts"; +import { type FromSql, SqlValue } from "./sql.ts"; + +export interface Row extends Iterable { + [column: string]: unknown; +} + +export interface RowConstructor { + new (columns: (Uint8Array | string | null)[]): Row; +} + +export interface RowDescription extends ReadonlyArray {} + +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`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 = readonly [...T]; + +export interface CommandResult { + readonly tag: string; +} + +export interface Result extends CommandResult, ReadonlyTuple<[T]> { + readonly row: T; +} + +export interface Results extends CommandResult, ReadonlyArray { + readonly rows: ReadonlyArray; +} + +export interface ResultStream + extends AsyncIterable {} + +export interface QueryOptions { + readonly chunk_size: number; + readonly stdin: ReadableStream | null; + readonly stdout: WritableStream | null; +} + +export class Query + implements PromiseLike>, ResultStream +{ + readonly #f; + + constructor(f: (options: Partial) => ResultStream) { + this.#f = f; + } + + chunked(chunk_size = 1) { + const f = this.#f; + return new Query((o) => f({ chunk_size, ...o })); + } + + stdin(stdin: ReadableStream | string | null) { + if (typeof stdin === "string") stdin = str_to_stream(stdin); + const f = this.#f; + return new Query((o) => f({ stdin, ...o })); + } + + stdout(stdout: WritableStream | null) { + const f = this.#f; + return new Query((o) => f({ stdout, ...o })); + } + + map(f: (row: T, index: number) => S) { + // deno-lint-ignore no-this-alias + const q = this; + return new Query(async function* map(o) { + const iter = q.#f(o)[Symbol.asyncIterator](); + let i, next; + for (i = 0; !(next = await iter.next()).done; ) { + const { value: from } = next; + const to = []; + for (let j = 0, n = (to.length = from.length); j < n; j++) { + to[j] = f(from[j], i++); + } + yield to; + } + return next.value; + }); + } + + filter(f: (row: T, index: number) => row is S) { + // deno-lint-ignore no-this-alias + const q = this; + return new Query(async function* filter(o) { + const iter = q.#f(o)[Symbol.asyncIterator](); + let i, next; + for (i = 0; !(next = await iter.next()).done; ) { + const { value: from } = next; + const to = []; + for (let j = 0, k = 0, n = from.length; j < n; j++) { + const x = from[j]; + if (f(x, i++)) to[k++] = x; + } + yield to; + } + return next.value; + }); + } + + parse( + type: S, + { mode = "strip" }: { mode?: "passthrough" | "strict" | "strip" } = {} + ) { + return this.map(function parse(row) { + return type.parse(row, { mode }); + }); + } + + stream(options: Partial = {}) { + return this.#f(options); + } + + async first(): Promise> { + 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_or(value: S): Promise> { + 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 collect(count = Number.POSITIVE_INFINITY): Promise> { + 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++]; + } + return Object.assign(rows, next.value, { rows }); + } + + async execute() { + const iter = this[Symbol.asyncIterator](); + let next; + while (!(next = await iter.next()).done); + return next.value; + } + + async count() { + const iter = this[Symbol.asyncIterator](); + let n = 0; + for (let next; !(next = await iter.next()).done; ) n += next.value.length; + return n; + } + + then, U = never>( + f?: ((rows: Results) => S | PromiseLike) | null, + g?: ((reason?: unknown) => U | PromiseLike) | null + ) { + return this.collect().then(f, g); + } + + [Symbol.asyncIterator]() { + return this.stream()[Symbol.asyncIterator](); + } +} + +function str_to_stream(s: string) { + return new ReadableStream({ + type: "bytes", + start(c) { + c.enqueue(to_utf8(s)), c.close(); + }, + }); +} diff --git a/ser.ts b/ser.ts new file mode 100644 index 0000000..28b0b28 --- /dev/null +++ b/ser.ts @@ -0,0 +1,271 @@ +import { encode_utf8, from_utf8, jit } from "./lstd.ts"; + +export class EncoderError extends Error { + override get name() { + return this.constructor.name; + } +} + +export interface Cursor { + i: number; +} + +export function ser_encode(type: Encoder, x: T) { + const buf = new Uint8Array(type.const_size ?? type.allocs(x)); + const cur: Cursor = { i: 0 }; + return type.encode(buf, cur, x), buf.subarray(0, cur.i); +} + +export function ser_decode(type: Encoder, buf: Uint8Array) { + return type.decode(buf, { i: 0 }); +} + +export interface Encoder { + readonly const_size: number | null; + allocs(value: T): number; + encode(buf: Uint8Array, cur: Cursor, value: T): void; + decode(buf: Uint8Array, cur: Cursor): T; +} + +export type EncoderType> = + E extends Encoder ? 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 = { + const_size: 1, + allocs() { + return 1; + }, + encode(buf, cur, n) { + buf[cur.i++] = n & 0xff; + }, + decode(buf, cur) { + return buf[cur.i++]; + }, +}; + +export const u16: Encoder = { + 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; + }, + decode(buf, cur) { + let { i } = cur; + const n = (buf[i++] << 8) + buf[i++]; + return (cur.i = i), n; + }, +}; + +export const i32: Encoder = { + const_size: 4, + allocs() { + 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; + }, + decode(buf, cur) { + let { i } = cur; + const n = (buf[i++] << 24) + (buf[i++] << 16) + (buf[i++] << 8) + buf[i++]; + return (cur.i = i), n; + }, +}; + +export function char(type: Encoder) { + return map(type, { + from(n: number) { + return n === 0 ? "" : String.fromCharCode(n); + }, + to(s: string) { + return s === "" ? 0 : s.charCodeAt(0); + }, + }); +} + +export function byten(n: number): Encoder { + return { + const_size: n, + allocs() { + return n; + }, + encode(buf, cur, s) { + if (s.length === n) buf.set(s, cur.i), (cur.i += n); + else throw new EncoderError(`buffer size must be ${n}`); + }, + decode(buf, cur) { + return buf.subarray(cur.i, (cur.i += n)); + }, + }; +} + +export const byten_lp: Encoder = { + const_size: null, + allocs(s) { + let size = 4; + if (typeof s === "string") size += s.length * 3; + else if (s !== null) size += s.length; + return size; + }, + encode(buf, cur, s) { + if (s === null) { + i32.encode(buf, cur, -1); + } else { + const n = encode_utf8(s, buf.subarray(cur.i + 4)); + i32.encode(buf, cur, n), (cur.i += n); + } + }, + decode(buf, cur) { + const n = i32.decode(buf, cur); + return n === -1 ? null : buf.subarray(cur.i, (cur.i += n)); + }, +}; + +export const byten_rest: Encoder = { + 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 = { + const_size: null, + allocs(s) { + return s.length * 3 + 1; + }, + encode(buf, cur, s) { + if (s.indexOf("\0") !== -1) + throw new EncoderError(`cstring must not contain a null byte`); + cur.i += encode_utf8(s, buf.subarray(cur.i)) + 1; + }, + decode(buf, cur) { + const end = buf.indexOf(0, cur.i); + if (end === -1) throw new EncoderError(`unexpected end of cstring`); + return from_utf8(buf.subarray(cur.i, (cur.i = end + 1) - 1)); + }, +}; + +export function map( + type: Encoder, + { from, to }: { from: (value: T) => U; to: (value: U) => T } +): Encoder { + return { + const_size: type.const_size, + allocs(x) { + return type.allocs(to(x)); + }, + encode(buf, cur, x) { + type.encode(buf, cur, to(x)); + }, + decode(buf, cur) { + return from(type.decode(buf, cur)); + }, + }; +} + +export function oneof( + type: Encoder, + ...xs: C +): Encoder { + const set = new Set(xs); + const exp = xs.map((c) => `'${c}'`).join(", "); + return map(type, { + from(x) { + if (set.has(x)) return x; + else throw new EncoderError(`expected ${exp}, got '${x}' instead`); + }, + to(x) { + if (set.has(x)) return x; + else throw new EncoderError(`expected ${exp}, got '${x}' instead`); + }, + }); +} + +export interface ArrayEncoder extends Encoder> {} + +export function array( + len_type: Encoder, + type: Encoder +): ArrayEncoder { + const { const_size } = type; + return { + const_size, + allocs: + const_size !== null + ? function allocs(xs: T[]) { + const n = xs.length; + return len_type.allocs(n) + n * const_size; + } + : function allocs(xs: T[]) { + const n = xs.length; + let size = len_type.allocs(n); + for (let i = 0; i < n; i++) size += type.allocs(xs[i]); + return size; + }, + encode(buf, cur, xs) { + const n = xs.length; + len_type.encode(buf, cur, n); + for (let i = 0; i < n; i++) type.encode(buf, cur, xs[i]); + }, + decode(buf, cur) { + const xs = []; + for (let i = 0, n = (xs.length = len_type.decode(buf, cur)); i < n; i++) + xs[i] = type.decode(buf, cur); + return xs; + }, + }; +} + +export type ObjectShape = Record>; +export interface ObjectEncoder + extends Encoder<{ [K in keyof S]: EncoderType }> {} + +export function object(shape: S): ObjectEncoder { + const keys = Object.keys(shape); + return jit.compiled`{ + const_size: ${jit.literal(sum_const_size(...keys.map((k) => shape[k].const_size)))}, + allocs(x) { + return 0${jit.map("", keys, (k) => { + return jit` + ${shape[k]}.allocs(x[${jit.literal(k)}])`; + })}; + }, + encode(buf, cur, x) { + ${jit.map(" ", keys, (k) => { + return jit`${shape[k]}.encode(buf, cur, x[${jit.literal(k)}]);`; + })} + }, + decode(buf, cur) { + return { + ${jit.map(", ", keys, (k) => { + return jit`[${jit.literal(k)}]: ${shape[k]}.decode(buf, cur)`; + })} + }; + }, + }`; +} diff --git a/sql.ts b/sql.ts new file mode 100644 index 0000000..9d7bdc9 --- /dev/null +++ b/sql.ts @@ -0,0 +1,386 @@ +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( + sep: string | SqlFragment, + xs: Iterable, + 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) { + 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); +}; diff --git a/valita.ts b/valita.ts new file mode 100644 index 0000000..a75402a --- /dev/null +++ b/valita.ts @@ -0,0 +1 @@ +export * from "jsr:@badrap/valita@^0.4.2"; diff --git a/wire.ts b/wire.ts new file mode 100644 index 0000000..22084dc --- /dev/null +++ b/wire.ts @@ -0,0 +1,1421 @@ +import { + buf_concat_fast, + buf_eq, + buf_xor, + channel, + from_base64, + from_utf8, + semaphore, + semaphore_fast, + to_base64, + to_utf8, + TypedEmitter, +} from "./lstd.ts"; +import { + array, + byten, + byten_lp, + byten_rest, + char, + cstring, + type Encoder, + object, + type ObjectEncoder, + type ObjectShape, + oneof, + ser_decode, + ser_encode, + u16, + i32, + u8, + sum_const_size, +} from "./ser.ts"; +import { + is_sql, + sql, + type FromSql, + type SqlFragment, + type ToSql, +} from "./sql.ts"; +import { + type CommandResult, + Query, + type ResultStream, + type Row, + row_ctor, + type RowConstructor, +} from "./query.ts"; +import { join } from "jsr:@std/path@^1.0.8"; + +export class WireError extends Error { + override get name() { + return this.constructor.name; + } +} + +export class PostgresError extends WireError { + readonly severity; + readonly code; + readonly detail; + readonly hint; + readonly position; + readonly where; + readonly schema; + readonly table; + readonly column; + readonly data_type; + readonly constraint; + readonly file; + readonly line; + readonly routine; + + constructor(fields: Partial>) { + // https://www.postgresql.org/docs/current/protocol-error-fields.html#PROTOCOL-ERROR-FIELDS + const { S, V, C, M, D, H, P, W, s, t, c, d, n, F, L, R } = fields; + super(M ?? "unknown error"); + this.severity = V ?? S ?? "ERROR"; + this.code = C ?? "XX000"; + this.detail = D ?? null; + this.hint = H ?? null; + this.position = P ?? null; + this.where = W ?? null; + this.schema = s ?? null; + this.table = t ?? null; + this.column = c ?? null; + this.data_type = d ?? null; + this.constraint = n ?? null; + this.file = F ?? null; + this.line = L ? parseInt(L, 10) : null; + this.routine = R ?? null; + } +} + +function severity_level(s: string): LogLevel { + switch (s) { + case "DEBUG": + return "debug"; + default: + case "LOG": + case "INFO": + case "NOTICE": + return "info"; + case "WARNING": + return "warn"; + case "ERROR": + return "error"; + case "FATAL": + case "PANIC": + return "fatal"; + } +} + +interface MessageEncoder + extends ObjectEncoder { + readonly type: T; +} + +function msg( + type: T, + shape: S +): MessageEncoder { + const header_size = type !== "" ? 5 : 4; + const ty = type !== "" ? oneof(char(u8), type) : null; + const fields = object(shape); + + return { + const_size: sum_const_size(header_size, fields.const_size), + get type() { + return type; + }, + allocs(msg) { + return header_size + fields.allocs(msg); + }, + encode(buf, cur, msg) { + ty?.encode(buf, cur, type); + const { i } = cur; + cur.i += 4; + fields.encode(buf, cur, msg); + i32.encode(buf, { i }, cur.i - i); + }, + decode(buf, cur) { + ty?.decode(buf, cur); + const n = i32.decode(buf, cur) - 4; + return fields.decode(buf.subarray(cur.i, (cur.i += n)), { i: 0 }); + }, + }; +} + +function msg_type({ 0: n }: Uint8Array) { + return n === 0 ? "" : String.fromCharCode(n); +} + +function msg_check_err(msg: Uint8Array) { + if (msg_type(msg) === ErrorResponse.type) { + const { fields } = ser_decode(ErrorResponse, msg); + throw new PostgresError(fields); + } else { + return msg; + } +} + +// https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS +export const Header = object({ + type: char(u8), + length: i32, +}); + +export const Authentication = msg("R", { + status: i32, +}); + +export const AuthenticationOk = msg("R", { + status: oneof(i32, 0 as const), +}); + +export const AuthenticationKerberosV5 = msg("R", { + status: oneof(i32, 2 as const), +}); + +export const AuthenticationCleartextPassword = msg("R", { + status: oneof(i32, 3 as const), +}); + +export const AuthenticationMD5Password = msg("R", { + status: oneof(i32, 5 as const), + salt: byten(4), +}); + +export const AuthenticationGSS = msg("R", { + status: oneof(i32, 7 as const), +}); + +export const AuthenticationGSSContinue = msg("R", { + status: oneof(i32, 8 as const), + data: byten_rest, +}); + +export const AuthenticationSSPI = msg("R", { + status: oneof(i32, 9 as const), +}); + +export const AuthenticationSASL = msg("R", { + status: oneof(i32, 10 as const), + mechanisms: { + const_size: null, + allocs(x) { + let size = 1; + for (const s of x) size += cstring.allocs(s); + return size; + }, + encode(buf, cur, x) { + for (const s of x) cstring.encode(buf, cur, s); + cstring.encode(buf, cur, ""); + }, + decode(buf, cur) { + const x = []; + for (let s; (s = cstring.decode(buf, cur)) !== ""; ) x.push(s); + return x; + }, + } satisfies Encoder, +}); + +export const AuthenticationSASLContinue = msg("R", { + status: oneof(i32, 11 as const), + data: byten_rest, +}); + +export const AuthenticationSASLFinal = msg("R", { + status: oneof(i32, 12 as const), + data: byten_rest, +}); + +export const BackendKeyData = msg("K", { + process_id: i32, + secret_key: i32, +}); + +export const Bind = msg("B", { + portal: cstring, + statement: cstring, + param_formats: array(u16, u16), + param_values: array(u16, byten_lp), + column_formats: array(u16, u16), +}); + +export const BindComplete = msg("2", {}); + +export const CancelRequest = msg("", { + code: oneof(i32, 80877102 as const), + process_id: i32, + secret_key: i32, +}); + +export const Close = msg("C", { + which: oneof(char(u8), "S" as const, "P" as const), + name: cstring, +}); + +export const CloseComplete = msg("3", {}); +export const CommandComplete = msg("C", { tag: cstring }); +export const CopyData = msg("d", { data: byten_rest }); +export const CopyDone = msg("c", {}); +export const CopyFail = msg("f", { cause: cstring }); + +export const CopyInResponse = msg("G", { + format: u8, + column_formats: array(u16, u16), +}); + +export const CopyOutResponse = msg("H", { + format: u8, + column_formats: array(u16, u16), +}); + +export const CopyBothResponse = msg("W", { + format: u8, + column_formats: array(u16, u16), +}); + +export const DataRow = msg("D", { + column_values: array(u16, byten_lp), +}); + +export const Describe = msg("D", { + which: oneof(char(u8), "S" as const, "P" as const), + name: cstring, +}); + +export const EmptyQueryResponse = msg("I", {}); + +const err_field = char(u8); +const err_fields: Encoder> = { + const_size: null, + allocs(x) { + let size = 1; + for (const { 0: key, 1: value } of Object.entries(x)) { + size += err_field.allocs(key) + cstring.allocs(value); + } + return size; + }, + encode(buf, cur, x) { + for (const { 0: key, 1: value } of Object.entries(x)) { + err_field.encode(buf, cur, key), cstring.encode(buf, cur, value); + } + err_field.encode(buf, cur, ""); + }, + decode(buf, cur) { + const x: Record = {}; + for (let key; (key = err_field.decode(buf, cur)) !== ""; ) { + x[key] = cstring.decode(buf, cur); + } + return x; + }, +}; + +export const ErrorResponse = msg("E", { + fields: err_fields, +}); + +export const Execute = msg("E", { + portal: cstring, + row_limit: i32, +}); + +export const Flush = msg("H", {}); + +export const FunctionCall = msg("F", { + oid: i32, + arg_formats: array(u16, u16), + arg_values: array(u16, byten_lp), + result_format: u16, +}); + +export const FunctionCallResponse = msg("V", { + result_value: byten_lp, +}); + +export const NegotiateProtocolVersion = msg("v", { + minor_ver: i32, + bad_options: array(i32, cstring), +}); + +export const NoData = msg("n", {}); + +export const NoticeResponse = msg("N", { + fields: err_fields, +}); + +export const NotificationResponse = msg("A", { + process_id: i32, + channel: cstring, + payload: cstring, +}); + +export const ParameterDescription = msg("t", { + param_types: array(u16, i32), +}); + +export const ParameterStatus = msg("S", { + name: cstring, + value: cstring, +}); + +export const Parse = msg("P", { + statement: cstring, + query: cstring, + param_types: array(u16, i32), +}); + +export const ParseComplete = msg("1", {}); + +export const PasswordMessage = msg("p", { + password: cstring, +}); + +export const PortalSuspended = msg("s", {}); + +export const QueryMessage = msg("Q", { + query: cstring, +}); + +export const ReadyForQuery = msg("Z", { + tx_status: oneof(char(u8), "I" as const, "T" as const, "E" as const), +}); + +export const RowDescription = msg("T", { + columns: array( + u16, + object({ + name: cstring, + table_oid: i32, + table_column: u16, + type_oid: i32, + type_size: u16, + type_modifier: i32, + format: u16, + }) + ), +}); + +export const SASLInitialResponse = msg("p", { + mechanism: cstring, + data: byten_lp, +}); + +export const SASLResponse = msg("p", { + data: byten_rest, +}); + +export const StartupMessage = msg("", { + version: oneof(i32, 196608 as const), + params: { + const_size: null, + allocs(x) { + let size = 1; + for (const { 0: key, 1: value } of Object.entries(x)) { + size += cstring.allocs(key) + cstring.allocs(value); + } + return size; + }, + encode(buf, cur, x) { + for (const { 0: key, 1: value } of Object.entries(x)) { + cstring.encode(buf, cur, key), cstring.encode(buf, cur, value); + } + u8.encode(buf, cur, 0); + }, + decode(buf, cur) { + const x: Record = {}; + for (let key; (key = cstring.decode(buf, cur)) !== ""; ) { + x[key] = cstring.decode(buf, cur); + } + return x; + }, + } satisfies Encoder>, +}); + +export const Sync = msg("S", {}); +export const Terminate = msg("X", {}); + +export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal"; + +export interface Parameters extends Readonly>> {} + +export interface WireOptions { + readonly host: string; + readonly port: number; + readonly user: string; + readonly password: string; + readonly database: string | null; + readonly runtime_params: Record; + readonly from_sql: FromSql; + readonly to_sql: ToSql; +} + +export type WireEvents = { + log(level: LogLevel, ctx: object, msg: string): void; + notice(notice: PostgresError): void; + parameter(name: string, value: string, prev: string | null): void; + notify(channel: string, payload: string, process_id: number): void; + close(reason?: unknown): void; +}; + +export interface Transaction extends CommandResult, AsyncDisposable { + readonly open: boolean; + commit(): Promise; + rollback(): Promise; +} + +export type ChannelEvents = { notify: NotificationHandler }; +export type NotificationHandler = (payload: string, process_id: number) => void; +export interface Channel + extends TypedEmitter, + CommandResult, + AsyncDisposable { + readonly name: string; + readonly open: boolean; + notify(payload: string): Promise; + unlisten(): Promise; +} + +export async function wire_connect(options: WireOptions) { + const { host, port } = options; + const wire = new Wire(await socket_connect(host, port), options); + return await wire.connected, wire; +} + +async function socket_connect(hostname: string, port: number) { + if (hostname.startsWith("/")) { + const path = join(hostname, `.s.PGSQL.${port}`); + return await Deno.connect({ transport: "unix", path }); + } else { + const socket = await Deno.connect({ transport: "tcp", hostname, port }); + return socket.setNoDelay(), socket.setKeepAlive(), socket; + } +} + +export class Wire extends TypedEmitter implements Disposable { + readonly #socket; + readonly #params; + readonly #auth; + readonly #connected; + readonly #query; + readonly #begin; + readonly #listen; + readonly #notify; + readonly #close; + + get socket() { + return this.#socket; + } + + get params() { + return this.#params; + } + + get connected() { + return this.#connected; + } + + constructor(socket: Deno.Conn, options: WireOptions) { + super(); + ({ + params: this.#params, + auth: this.#auth, + query: this.#query, + begin: this.#begin, + listen: this.#listen, + notify: this.#notify, + close: this.#close, + } = wire_impl(this, socket, options)); + this.#socket = socket; + (this.#connected = this.#auth()).catch(close); + } + + query(sql: SqlFragment): Query; + query(s: TemplateStringsArray, ...xs: unknown[]): Query; + query(s: TemplateStringsArray | SqlFragment, ...xs: unknown[]) { + return this.#query(is_sql(s) ? s : sql(s, ...xs)); + } + + begin(): Promise; + begin(f: (wire: this, tx: Transaction) => T | PromiseLike): Promise; + async begin(f?: (wire: this, tx: Transaction) => unknown) { + if (typeof f !== "undefined") { + await using tx = await this.#begin(); + const value = await f(this, tx); + return await tx.commit(), value; + } else { + return this.#begin(); + } + } + + async listen(channel: string, ...fs: NotificationHandler[]) { + const ch = await this.#listen(channel); + for (const f of fs) ch.on("notify", f); + return ch; + } + + notify(channel: string, payload: string) { + return this.#notify(channel, payload); + } + + async get(param: string, missing_null = true) { + return ( + await this.query`select current_setting(${param}, ${missing_null})` + .map(([s]) => String(s)) + .first_or(null) + )[0]; + } + + async set(param: string, value: string, local = false) { + return await this + .query`select set_config(${param}, ${value}, ${local})`.execute(); + } + + close(reason?: unknown) { + this.#close(reason); + } + + [Symbol.dispose]() { + this.close(); + } +} + +function wire_impl( + wire: Wire, + socket: Deno.Conn, + { user, database, password, runtime_params, from_sql, to_sql }: WireOptions +) { + const params: Parameters = Object.create(null); + + function log(level: LogLevel, ctx: object, msg: string) { + wire.emit("log", level, ctx, msg); + } + + async function read(type: Encoder) { + const msg = await read_recv(); + if (msg === null) throw new WireError(`connection closed`); + else return ser_decode(type, msg_check_err(msg)); + } + + async function read_raw() { + const msg = await read_recv(); + if (msg === null) throw new WireError(`connection closed`); + else return msg; + } + + async function* read_socket() { + const buf = new Uint8Array(64 * 1024); + for (let n; (n = await socket.read(buf)) !== null; ) + yield buf.subarray(0, n); + } + + const read_recv = channel.receiver(async function read(send) { + try { + let buf = new Uint8Array(); + for await (const chunk of read_socket()) { + buf = buf_concat_fast(buf, chunk); + + for (let n; (n = ser_decode(Header, buf).length + 1) <= buf.length; ) { + const msg = buf.subarray(0, n); + buf = buf.subarray(n); + + switch (msg_type(msg)) { + // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-ASYNC + case NoticeResponse.type: { + const { fields } = ser_decode(NoticeResponse, msg); + const notice = new PostgresError(fields); + log(severity_level(notice.severity), notice, notice.message); + wire.emit("notice", notice); + continue; + } + + case ParameterStatus.type: { + const { name, value } = ser_decode(ParameterStatus, msg); + const prev = params[name] ?? null; + Object.defineProperty(params, name, { + configurable: true, + enumerable: true, + value, + }); + wire.emit("parameter", name, value, prev); + continue; + } + + case NotificationResponse.type: { + const { channel, payload, process_id } = ser_decode( + NotificationResponse, + msg + ); + wire.emit("notify", channel, payload, process_id); + channels.get(channel)?.emit("notify", payload, process_id); + continue; + } + } + + send(msg); + } + } + + if (buf.length !== 0) throw new WireError(`unexpected end of stream`); + wire.emit("close"); + } catch (e) { + wire.emit("close", e); + } + }); + + function write(type: Encoder, value: T) { + return write_raw(ser_encode(type, value)); + } + + async function write_raw(buf: Uint8Array) { + for (let i = 0, n = buf.length; i < n; ) + i += await socket.write(buf.subarray(i)); + } + + function close(reason?: unknown) { + socket.close(), read_recv.close(reason); + } + + // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-PIPELINING + const rlock = semaphore_fast(); + const wlock = semaphore_fast(); + + function pipeline( + w: () => void | PromiseLike, + r: () => T | PromiseLike + ) { + return new Promise((res, rej) => { + pipeline_write(w).catch(rej); + pipeline_read(r).then(res, rej); + }); + } + + function pipeline_read(r: () => T | PromiseLike) { + return rlock(async () => { + try { + return await r(); + } finally { + let msg; + while (msg_type((msg = await read_raw())) !== ReadyForQuery.type); + ({ tx_status } = ser_decode(ReadyForQuery, msg)); + } + }); + } + + function pipeline_write(w: () => T | PromiseLike) { + return wlock(async () => { + try { + return await w(); + } finally { + await write(Sync, {}); + } + }); + } + + // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-START-UP + async function auth() { + await write(StartupMessage, { + version: 196608, + params: { + application_name: "pglue", + idle_session_timeout: "0", + ...runtime_params, + user, + database: database ?? user, + bytea_output: "hex", + client_encoding: "utf8", + DateStyle: "ISO", + }, + }); + + auth: for (;;) { + const msg = msg_check_err(await read_raw()); + switch (msg_type(msg)) { + case NegotiateProtocolVersion.type: { + const { bad_options } = ser_decode(NegotiateProtocolVersion, msg); + log("info", { bad_options }, `unrecognised protocol options`); + continue; + } + } + + const { status } = ser_decode(Authentication, msg); + switch (status) { + case 0: // AuthenticationOk + break auth; + + case 2: // AuthenticationKerberosV5 + throw new WireError(`kerberos authentication is deprecated`); + + case 3: // AuthenticationCleartextPassword + await write(PasswordMessage, { password }); + continue; + + case 5: // AuthenticationMD5Password + throw new WireError( + `md5 password authentication is deprecated (prefer scram-sha-256 instead)` + ); + + case 7: // AuthenticationGSS + throw new WireError(`gssapi authentication is not supported`); + + case 9: // AuthenticationSSPI + throw new WireError(`sspi authentication is not supported`); + + // AuthenticationSASL + case 10: + await auth_sasl(); + continue; + + default: + throw new WireError(`invalid authentication status ${status}`); + } + } + + ready: for (;;) { + const msg = msg_check_err(await read_raw()); + switch (msg_type(msg)) { + case BackendKeyData.type: + continue; // ignored + + default: + ser_decode(ReadyForQuery, msg); + break ready; + } + } + } + + // https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256 + // https://datatracker.ietf.org/doc/html/rfc5802 + async function auth_sasl() { + const bits = 256; + const hash = `SHA-${bits}`; + const mechanism = `SCRAM-${hash}`; + + async function hmac(key: Uint8Array, str: string | Uint8Array) { + return new Uint8Array( + await crypto.subtle.sign( + "HMAC", + await crypto.subtle.importKey( + "raw", + key, + { name: "HMAC", hash }, + false, + ["sign"] + ), + to_utf8(str) + ) + ); + } + + async function h(str: string | Uint8Array) { + return new Uint8Array(await crypto.subtle.digest(hash, to_utf8(str))); + } + + async function hi(str: string | Uint8Array, salt: Uint8Array, i: number) { + return new Uint8Array( + await crypto.subtle.deriveBits( + { name: "PBKDF2", hash, salt, iterations: i }, + await crypto.subtle.importKey("raw", to_utf8(str), "PBKDF2", false, [ + "deriveBits", + ]), + bits + ) + ); + } + + function parse_attrs(s: string) { + const attrs: Partial> = {}; + for (const entry of s.split(",")) { + const { 0: name, 1: value = "" } = entry.split("=", 2); + attrs[name] = value; + } + return attrs; + } + + const gs2_cbind_flag = `n`; + const gs2_header = `${gs2_cbind_flag},,`; + const username = `n=*`; + const cbind_data = ``; + const cbind_input = `${gs2_header}${cbind_data}`; + const channel_binding = `c=${to_base64(cbind_input)}`; + const initial_nonce = `r=${to_base64( + crypto.getRandomValues(new Uint8Array(18)) + )}`; + const client_first_message_bare = `${username},${initial_nonce}`; + const client_first_message = `${gs2_header}${client_first_message_bare}`; + await write(SASLInitialResponse, { mechanism, data: client_first_message }); + + const server_first_message_str = from_utf8( + (await read(AuthenticationSASLContinue)).data + ); + const server_first_message = parse_attrs(server_first_message_str); + const nonce = `r=${server_first_message.r ?? ""}`; + if (!nonce.startsWith(initial_nonce)) throw new WireError(`bad nonce`); + const salt = from_base64(server_first_message.s ?? ""); + const iters = parseInt(server_first_message.i ?? "", 10) || 0; + const salted_password = await hi(password, salt, iters); + const client_key = await hmac(salted_password, "Client Key"); + const stored_key = await h(client_key); + const client_final_message_without_proof = `${channel_binding},${nonce}`; + const auth_message = `${client_first_message_bare},${server_first_message_str},${client_final_message_without_proof}`; + const client_signature = await hmac(stored_key, auth_message); + const client_proof = buf_xor(client_key, client_signature); + const proof = `p=${to_base64(client_proof)}`; + const client_final_message = `${client_final_message_without_proof},${proof}`; + await write(SASLResponse, { data: client_final_message }); + + const server_key = await hmac(salted_password, "Server Key"); + const server_signature = await hmac(server_key, auth_message); + const server_final_message = parse_attrs( + from_utf8((await read(AuthenticationSASLFinal)).data) + ); + + if (!buf_eq(from_base64(server_final_message.v ?? ""), server_signature)) + throw new WireError(`SASL server signature mismatch`); + } + + // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY + const st_cache = new Map(); + let st_ids = 0; + + function st_get(query: string, param_types: number[]) { + const key = JSON.stringify({ q: query, p: param_types }); + let st = st_cache.get(key); + if (!st) st_cache.set(key, (st = new Statement(query, param_types))); + return st; + } + + class Statement { + readonly name = `__st${st_ids++}`; + + constructor( + readonly query: string, + readonly param_types: number[] + ) {} + + parse_task: Promise | null = null; + parse() { + return (this.parse_task ??= this.#parse()); + } + + async #parse() { + try { + const { name, query, param_types } = this; + return row_ctor( + from_sql, + await pipeline( + async () => { + await write(Parse, { statement: name, query, param_types }); + await write(Describe, { which: "S", name }); + }, + async () => { + await read(ParseComplete); + await read(ParameterDescription); + + const msg = msg_check_err(await read_raw()); + if (msg_type(msg) === NoData.type) return []; + else return ser_decode(RowDescription, msg).columns; + } + ) + ); + } catch (e) { + throw ((this.parse_task = null), e); + } + } + + portals = 0; + portal() { + return `${this.name}_${this.portals++}`; + } + } + + async function read_rows( + Row: RowConstructor, + stdout: WritableStream | null + ) { + for (let rows = [], i = 0; ; ) { + const msg = msg_check_err(await read_raw()); + switch (msg_type(msg)) { + default: + case DataRow.type: + rows[i++] = new Row(ser_decode(DataRow, msg).column_values); + continue; + + case CommandComplete.type: { + const { tag } = ser_decode(CommandComplete, msg); + return { done: true as const, rows, tag }; + } + + case PortalSuspended.type: + return { done: false as const, rows, tag: "" }; + + case EmptyQueryResponse.type: + return { done: true as const, rows, tag: "" }; + + case CopyInResponse.type: + continue; + + case CopyOutResponse.type: + await read_copy_out(stdout); + continue; + } + } + } + + async function read_copy_out(stream: WritableStream | null) { + if (stream !== null) { + const writer = stream.getWriter(); + try { + for (let msg; msg_type((msg = await read_raw())) !== CopyDone.type; ) { + const { data } = ser_decode(CopyData, msg_check_err(msg)); + await writer.write(to_utf8(data)); + } + } finally { + writer.releaseLock(); + } + } else { + while (msg_type(msg_check_err(await read_raw())) !== CopyDone.type); + } + } + + async function write_copy_in(stream: ReadableStream | null) { + if (stream !== null) { + const reader = stream.getReader(); + let err; + try { + try { + for (let next; !(next = await reader.read()).done; ) + await write(CopyData, { data: next.value }); + } catch (e) { + err = e; + } finally { + if (typeof err === "undefined") await write(CopyDone, {}); + else await write(CopyFail, { cause: String(err) }); + } + } finally { + reader.releaseLock(); + } + } else { + await write(CopyDone, {}); + } + } + + async function* execute_fast( + st: Statement, + params: { types: number[]; values: (string | null)[] }, + stdin: ReadableStream | null, + stdout: WritableStream | null + ): ResultStream { + log( + "debug", + { query: st.query, statement: st.name, params }, + `executing query` + ); + + const Row = await st.parse(); + const portal = st.portal(); + + try { + const { rows, tag } = await pipeline( + async () => { + await write(Bind, { + portal, + statement: st.name, + param_formats: [], + param_values: params.values, + column_formats: [], + }); + await write(Execute, { portal, row_limit: 0 }); + await write_copy_in(stdin); + await write(Close, { which: "P" as const, name: portal }); + }, + async () => { + await read(BindComplete); + return read_rows(Row, stdout); + } + ); + + if (rows.length) yield rows; + return { tag }; + } catch (e) { + await pipeline( + () => write(Close, { which: "P" as const, name: portal }), + () => read(CloseComplete) + ); + + throw e; + } + } + + async function* execute_chunked( + st: Statement, + params: { types: number[]; values: (string | null)[] }, + chunk_size: number, + stdin: ReadableStream | null, + stdout: WritableStream | null + ): ResultStream { + log( + "debug", + { query: st.query, statement: st.name, params }, + `executing chunked query` + ); + + const Row = await st.parse(); + const portal = st.portal(); + + try { + let { done, rows, tag } = await pipeline( + async () => { + await write(Bind, { + portal, + statement: st.name, + param_formats: [], + param_values: params.values, + column_formats: [], + }); + await write(Execute, { portal, row_limit: chunk_size }); + await write_copy_in(stdin); + }, + async () => { + await read(BindComplete); + return read_rows(Row, stdout); + } + ); + + if (rows.length) yield rows; + + while (!done) { + ({ done, rows, tag } = await pipeline( + () => write(Execute, { portal, row_limit: chunk_size }), + () => read_rows(Row, stdout) + )); + + if (rows.length) yield rows; + } + + return { tag }; + } finally { + await pipeline( + () => write(Close, { which: "P" as const, name: portal }), + () => read(CloseComplete) + ); + } + } + + function query(s: SqlFragment) { + const { query, params } = sql.format(s, to_sql); + const st = st_get(query, params.types); + + return new Query(({ chunk_size = 0, stdin = null, stdout = null }) => + chunk_size !== 0 + ? execute_chunked(st, params, chunk_size, stdin, stdout) + : execute_fast(st, params, stdin, stdout) + ); + } + + // https://www.postgresql.org/docs/current/sql-begin.html + // https://www.postgresql.org/docs/current/sql-savepoint.html + let tx_status: "I" | "T" | "E" = "I"; + const tx_stack: Transaction[] = []; + const tx_begin = query(sql`begin`); + const tx_commit = query(sql`commit`); + const tx_rollback = query(sql`rollback`); + const sp_savepoint = query(sql`savepoint __tx`); + const sp_release = query(sql`release __tx`); + const sp_rollback_to = query(sql`rollback to __tx`); + + async function begin() { + const tx = new Transaction( + await (tx_stack.length ? sp_savepoint.execute() : tx_begin.execute()) + ); + return tx_stack.push(tx), tx; + } + + const Transaction = class implements Transaction { + readonly tag!: string; + + get open(): boolean { + return tx_stack.indexOf(this) !== -1; + } + + constructor(begin: CommandResult) { + Object.assign(this, begin); + } + + async commit() { + const i = tx_stack.indexOf(this); + if (i === -1) throw new WireError(`transaction is not open`); + else tx_stack.length = i; + return await (i ? sp_release.execute() : tx_commit.execute()); + } + + async rollback() { + const i = tx_stack.indexOf(this); + if (i === -1) throw new WireError(`transaction is not open`); + else tx_stack.length = i; + if (i !== 0) { + const res = await sp_rollback_to.execute(); + return await sp_release.execute(), res; + } else { + return await tx_rollback.execute(); + } + } + + async [Symbol.asyncDispose]() { + if (this.open) await this.rollback(); + } + }; + + // https://www.postgresql.org/docs/current/sql-listen.html + // https://www.postgresql.org/docs/current/sql-notify.html + const channels = new Map(); + + async function listen(channel: string) { + let ch; + if ((ch = channels.get(channel))) return ch; + const res = await query(sql`listen ${sql.ident(channel)}`).execute(); + if (tx_status !== "I") + log("warn", {}, `LISTEN executed inside transaction`); + if ((ch = channels.get(channel))) return ch; + return channels.set(channel, (ch = new Channel(channel, res))), ch; + } + + async function notify(channel: string, payload: string) { + return await query(sql`select pg_notify(${channel}, ${payload})`).execute(); + } + + const Channel = class extends TypedEmitter implements Channel { + readonly #name; + readonly tag!: string; + + get name() { + return this.#name; + } + + get open(): boolean { + return channels.get(this.#name) === this; + } + + constructor(name: string, listen: CommandResult) { + super(); + Object.assign(this, listen); + this.#name = name; + } + + notify(payload: string) { + return notify(this.#name, payload); + } + + async unlisten() { + const name = this.#name; + if (channels.get(name) === this) channels.delete(name); + else throw new WireError(`channel is not listening`); + return await query(sql`unlisten ${sql.ident(name)}`).execute(); + } + + async [Symbol.asyncDispose]() { + if (this.open) await this.unlisten(); + } + }; + + return { params, auth, query, begin, listen, notify, close }; +} + +export interface PoolOptions extends WireOptions { + max_connections: number; + idle_timeout: number; +} + +export type PoolEvents = { + log(level: LogLevel, ctx: object, msg: string): void; +}; + +export interface PoolWire extends Wire { + readonly connection_id: number; + readonly borrowed: boolean; + release(): void; +} + +export interface PoolTransaction extends Transaction { + readonly wire: PoolWire; +} + +export class Pool + extends TypedEmitter + implements PromiseLike, Disposable +{ + readonly #acquire; + readonly #begin; + readonly #close; + + constructor(options: PoolOptions) { + super(); + ({ + acquire: this.#acquire, + begin: this.#begin, + close: this.#close, + } = pool_impl(this, options)); + } + + get(): Promise; + get(f: (wire: PoolWire) => T | PromiseLike): Promise; + async get(f?: (wire: PoolWire) => unknown) { + if (typeof f !== "undefined") { + using wire = await this.#acquire(); + return await f(wire); + } else { + return this.#acquire(); + } + } + + query(sql: SqlFragment): Query; + query(s: TemplateStringsArray, ...xs: unknown[]): Query; + query(s: TemplateStringsArray | SqlFragment, ...xs: unknown[]) { + s = is_sql(s) ? s : sql(s, ...xs); + const acquire = this.#acquire; + return new Query(async function* stream(options) { + using wire = await acquire(); + return yield* wire.query(s).stream(options); + }); + } + + begin(): Promise; + begin( + f: (wire: PoolWire, tx: PoolTransaction) => T | PromiseLike + ): Promise; + async begin(f?: (wire: PoolWire, tx: PoolTransaction) => unknown) { + if (typeof f !== "undefined") { + await using tx = await this.#begin(); + const value = await f(tx.wire, tx); + return await tx.commit(), value; + } else { + return this.#begin(); + } + } + + then( + f?: ((wire: PoolWire) => T | PromiseLike) | null, + g?: ((reason?: unknown) => U | PromiseLike) | null + ) { + return this.get().then(f, g); + } + + close() { + this.#close(); + } + + [Symbol.dispose]() { + this.close(); + } +} + +function pool_impl( + pool: Pool, + { max_connections, idle_timeout: _, ...options }: PoolOptions +) { + const lock = semaphore(max_connections); + const all = new Set(); + const free: PoolWire[] = []; + let ids = 0; + + const PoolWire = class extends Wire implements PoolWire { + readonly #id = ids++; + + get connection_id() { + return this.#id; + } + + get borrowed(): boolean { + return free.indexOf(this) === -1; + } + + release() { + if (all.has(this) && free.indexOf(this) === -1) + free.push(this), lock.release(); + } + + override [Symbol.dispose]() { + this.release(); + } + }; + + const PoolTransaction = class implements Transaction { + readonly #wire; + readonly #tx; + + get wire() { + return this.#wire; + } + + get tag() { + return this.#tx.tag; + } + + get open() { + return this.#tx.open; + } + + constructor(wire: PoolWire, tx: Transaction) { + this.#wire = wire; + this.#tx = tx; + } + + async commit() { + const res = await this.#tx.commit(); + return this.#wire.release(), res; + } + + async rollback() { + const res = await this.#tx.rollback(); + return this.#wire.release(), res; + } + + async [Symbol.asyncDispose]() { + if (this.open) await this.rollback(); + } + }; + + async function connect() { + const { host, port } = options; + const wire = new PoolWire(await socket_connect(host, port), options); + await wire.connected, all.add(wire); + const { connection_id } = wire; + return wire + .on("log", (l, c, s) => pool.emit("log", l, { ...c, connection_id }, s)) + .on("close", () => forget(wire)); + } + + async function acquire() { + await lock(); + try { + return free.pop() ?? (await connect()); + } catch (e) { + throw (lock.release(), e); + } + } + + function forget(wire: PoolWire) { + if (all.delete(wire)) { + const i = free.indexOf(wire); + if (i !== -1) free.splice(i, 1); + else lock.release(); + } + } + + async function begin() { + const wire = await acquire(); + try { + return new PoolTransaction(wire, await wire.begin()); + } catch (e) { + throw (wire.release(), e); + } + } + + function close() { + for (const wire of all) wire.close(); + all.clear(), (free.length = 0), lock.reset(max_connections); + } + + return { acquire, begin, close }; +}