+
+
+[](https://discord.com/invite/HEdTCvZUSf)
+[](https://jsr.io/@db/postgres)
+[](https://jsr.io/@db/postgres)
[](https://deno-postgres.com)
-[](https://doc.deno.land/https/deno.land/x/postgres/mod.ts)
+[](https://jsr.io/@db/postgres/doc)
[](LICENSE)
-A lightweight PostgreSQL driver for Deno focused on developer experience.
-
-`deno-postgres` is being developed inspired by the excellent work of
+A lightweight PostgreSQL driver for Deno focused on developer experience.\
+`deno-postgres` is inspired by the excellent work of
[node-postgres](https://github.com/brianc/node-postgres) and
[pq](https://github.com/lib/pq).
+
+
## Documentation
-The documentation is available on the `deno-postgres` website
-[https://deno-postgres.com/](https://deno-postgres.com/)
+The documentation is available on the
+[`deno-postgres`](https://deno-postgres.com/) website.
-Join the [Discord](https://discord.gg/HEdTCvZUSf) as well! It's a good place to
-discuss bugs and features before opening issues.
+Join the [Discord](https://discord.com/invite/HEdTCvZUSf) as well! It's a good
+place to discuss bugs and features before opening issues.
## Examples
```ts
// deno run --allow-net --allow-read mod.ts
-import { Client } from "https://deno.land/x/postgres/mod.ts";
+import { Client } from "jsr:@db/postgres";
const client = new Client({
user: "user",
@@ -32,6 +42,7 @@ const client = new Client({
hostname: "localhost",
port: 5432,
});
+
await client.connect();
{
@@ -59,6 +70,42 @@ await client.connect();
await client.end();
```
+## Deno compatibility
+
+Due to breaking changes introduced in the unstable APIs `deno-postgres` uses,
+there has been some fragmentation regarding what versions of Deno can be used
+alongside the driver.
+
+This situation will stabilize as `deno-postgres` approach version 1.0.
+
+| Deno version | Min driver version | Max version | Note |
+| ------------- | ------------------ | ----------- | -------------------------------------------------------------------------- |
+| 1.8.x | 0.5.0 | 0.10.0 | |
+| 1.9.0 | 0.11.0 | 0.11.1 | |
+| 1.9.1 and up | 0.11.2 | 0.11.3 | |
+| 1.11.0 and up | 0.12.0 | 0.12.0 | |
+| 1.14.0 and up | 0.13.0 | 0.13.0 | |
+| 1.16.0 | 0.14.0 | 0.14.3 | |
+| 1.17.0 | 0.15.0 | 0.17.1 | |
+| 1.40.0 | 0.17.2 | 0.19.3 | 0.19.3 and down are available in [deno.land](https://deno.land/x/postgres) |
+| 2.0.0 and up | 0.19.4 | - | Available on JSR! [`@db/postgres`](https://jsr.io/@db/postgres) |
+
+## Breaking changes
+
+Although `deno-postgres` is reasonably stable and robust, it is a WIP, and we're
+still exploring the design. Expect some breaking changes as we reach version 1.0
+and enhance the feature set. Please check the
+[Releases](https://github.com/denodrivers/postgres/releases) for more info on
+breaking changes. Please reach out if there are any undocumented breaking
+changes.
+
+## Found issues?
+
+Please
+[file an issue](https://github.com/denodrivers/postgres/issues/new/choose) with
+any problems with the driver. If you would like to help, please look at the
+issues as well. You can pick up one of them and try to implement it.
+
## Contributing
### Prerequisites
@@ -73,8 +120,8 @@ await client.end();
it to run the linter and formatter locally
- https://deno.land/
- - `deno upgrade --version 1.40.0`
- - `dvm install 1.40.0 && dvm use 1.40.0`
+ - `deno upgrade stable`
+ - `dvm install stable && dvm use stable`
- You don't need to install Postgres locally on your machine to test the
library; it will run as a service in the Docker container when you build it
@@ -86,8 +133,8 @@ result assertions.
To run the tests, run the following commands:
-1. `docker-compose build tests`
-2. `docker-compose run tests`
+1. `docker compose build tests`
+2. `docker compose run tests`
The build step will check linting and formatting as well and report it to the
command line
@@ -96,8 +143,8 @@ It is recommended that you don't rely on any previously initialized data for
your tests instead create all the data you need at the moment of running the
tests
-For example, the following test will create a temporal table that will disappear
-once the test has been completed
+For example, the following test will create a temporary table that will
+disappear once the test has been completed
```ts
Deno.test("INSERT works correctly", async () => {
@@ -134,41 +181,6 @@ a local testing environment, as shown in the following steps:
3. Run the tests manually by using the command\
`deno test -A`
-## Deno compatibility
-
-Due to breaking changes introduced in the unstable APIs `deno-postgres` uses,
-there has been some fragmentation regarding what versions of Deno can be used
-alongside the driver.
-
-This situation will stabilize as `std` and `deno-postgres` approach version 1.0.
-
-| Deno version | Min driver version | Max driver version | Note |
-| ------------- | ------------------ | ------------------ | -------------------- |
-| 1.8.x | 0.5.0 | 0.10.0 | |
-| 1.9.0 | 0.11.0 | 0.11.1 | |
-| 1.9.1 and up | 0.11.2 | 0.11.3 | |
-| 1.11.0 and up | 0.12.0 | 0.12.0 | |
-| 1.14.0 and up | 0.13.0 | 0.13.0 | |
-| 1.16.0 | 0.14.0 | 0.14.3 | |
-| 1.17.0 | 0.15.0 | 0.17.1 | |
-| 1.40.0 | 0.17.2 | | Now available on JSR |
-
-## Breaking changes
-
-Although `deno-postgres` is reasonably stable and robust, it is a WIP, and we're
-still exploring the design. Expect some breaking changes as we reach version 1.0
-and enhance the feature set. Please check the Releases for more info on breaking
-changes. Please reach out if there are any undocumented breaking changes.
-
-## Found issues?
-
-Please
-[file an issue](https://github.com/denodrivers/postgres/issues/new/choose) with
-any problems with the driver in this repository's issue section. If you would
-like to help, please look at the
-[issues](https://github.com/denodrivers/postgres/issues) as well. You can pick
-up one of them and try to implement it.
-
## Contributing guidelines
When contributing to the repository, make sure to:
@@ -194,5 +206,5 @@ preserved their individual licenses and copyrights.
Everything is licensed under the MIT License.
-All additional work is copyright 2018 - 2024 — Bartłomiej Iwańczuk, Steven
+All additional work is copyright 2018 - 2025 — Bartłomiej Iwańczuk, Steven
Guerrero, Hector Ayala — All rights reserved.
diff --git a/client.ts b/client.ts
index 7635c6a3..f064e976 100644
--- a/client.ts
+++ b/client.ts
@@ -105,47 +105,57 @@ export abstract class QueryClient {
* In order to create a transaction, use the `createTransaction` method in your client as follows:
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("my_transaction_name");
*
* await transaction.begin();
* // All statements between begin and commit will happen inside the transaction
* await transaction.commit(); // All changes are saved
+ * await client.end();
* ```
*
* All statements that fail in query execution will cause the current transaction to abort and release
* the client without applying any of the changes that took place inside it
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("cool_transaction");
*
* await transaction.begin();
- * await transaction.queryArray`INSERT INTO MY_TABLE (X) VALUES ${"some_value"}`;
+ *
* try {
- * await transaction.queryArray`SELECT []`; // Invalid syntax, transaction aborted, changes won't be applied
- * }catch(e){
- * await transaction.commit(); // Will throw, current transaction has already finished
+ * try {
+ * await transaction.queryArray`SELECT []`; // Invalid syntax, transaction aborted, changes won't be applied
+ * } catch (e) {
+ * await transaction.commit(); // Will throw, current transaction has already finished
+ * }
+ * } catch (e) {
+ * console.log(e);
* }
+ *
+ * await client.end();
* ```
*
* This however, only happens if the error is of execution in nature, validation errors won't abort
* the transaction
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("awesome_transaction");
*
* await transaction.begin();
- * await transaction.queryArray`INSERT INTO MY_TABLE (X) VALUES ${"some_value"}`;
+ *
* try {
* await transaction.rollback("unexistent_savepoint"); // Validation error
- * } catch(e) {
+ * } catch (e) {
+ * console.log(e);
* await transaction.commit(); // Transaction will end, changes will be saved
* }
+ *
+ * await client.end();
* ```
*
* A transaction has many options to ensure modifications made to the database are safe and
@@ -160,7 +170,7 @@ export abstract class QueryClient {
* - Repeatable read: This isolates the transaction in a way that any external changes to the data we are reading
* won't be visible inside the transaction until it has finished
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = await client.createTransaction("my_transaction", { isolation_level: "repeatable_read" });
* ```
@@ -168,7 +178,7 @@ export abstract class QueryClient {
* - Serializable: This isolation level prevents the current transaction from making persistent changes
* if the data they were reading at the beginning of the transaction has been modified (recommended)
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = await client.createTransaction("my_transaction", { isolation_level: "serializable" });
* ```
@@ -181,7 +191,7 @@ export abstract class QueryClient {
* is to in conjuction with the repeatable read isolation, ensuring the data you are reading does not change
* during the transaction, specially useful for data extraction
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = await client.createTransaction("my_transaction", { read_only: true });
* ```
@@ -192,14 +202,19 @@ export abstract class QueryClient {
* you can do the following:
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client_1 = new Client();
* const client_2 = new Client();
* const transaction_1 = client_1.createTransaction("transaction_1");
*
+ * await transaction_1.begin();
+ *
* const snapshot = await transaction_1.getSnapshot();
* const transaction_2 = client_2.createTransaction("new_transaction", { isolation_level: "repeatable_read", snapshot });
* // transaction_2 now shares the same starting state that transaction_1 had
+ *
+ * await client_1.end();
+ * await client_2.end();
* ```
*
* https://www.postgresql.org/docs/14/tutorial-transactions.html
@@ -260,9 +275,14 @@ export abstract class QueryClient {
* Execute queries and retrieve the data as array entries. It supports a generic in order to type the entries retrieved by the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
+ * await my_client.queryArray`CREATE TABLE IF NOT EXISTS CLIENTS (
+ * id SERIAL PRIMARY KEY,
+ * name TEXT NOT NULL
+ * )`
+ *
* const { rows: rows1 } = await my_client.queryArray(
* "SELECT ID, NAME FROM CLIENTS"
* ); // Array
@@ -270,6 +290,8 @@ export abstract class QueryClient {
* const { rows: rows2 } = await my_client.queryArray<[number, string]>(
* "SELECT ID, NAME FROM CLIENTS"
* ); // Array<[number, string]>
+ *
+ * await my_client.end();
* ```
*/
async queryArray>(
@@ -280,12 +302,13 @@ export abstract class QueryClient {
* Use the configuration object for more advance options to execute the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
* const { rows } = await my_client.queryArray<[number, string]>({
* text: "SELECT ID, NAME FROM CLIENTS",
* name: "select_clients",
* }); // Array<[number, string]>
+ * await my_client.end();
* ```
*/
async queryArray>(
@@ -295,12 +318,14 @@ export abstract class QueryClient {
* Execute prepared statements with template strings
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
* const id = 12;
* // Array<[number, string]>
* const {rows} = await my_client.queryArray<[number, string]>`SELECT ID, NAME FROM CLIENTS WHERE ID = ${id}`;
+ *
+ * await my_client.end();
* ```
*/
async queryArray>(
@@ -343,7 +368,7 @@ export abstract class QueryClient {
* Executed queries and retrieve the data as object entries. It supports a generic in order to type the entries retrieved by the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
* const { rows: rows1 } = await my_client.queryObject(
@@ -353,6 +378,8 @@ export abstract class QueryClient {
* const { rows: rows2 } = await my_client.queryObject<{id: number, name: string}>(
* "SELECT ID, NAME FROM CLIENTS"
* ); // Array<{id: number, name: string}>
+ *
+ * await my_client.end();
* ```
*/
async queryObject(
@@ -363,7 +390,7 @@ export abstract class QueryClient {
* Use the configuration object for more advance options to execute the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
* const { rows: rows1 } = await my_client.queryObject(
@@ -376,6 +403,8 @@ export abstract class QueryClient {
* fields: ["personal_id", "complete_name"],
* });
* console.log(rows2); // [{personal_id: 78, complete_name: "Frank"}, {personal_id: 15, complete_name: "Sarah"}]
+ *
+ * await my_client.end();
* ```
*/
async queryObject(
@@ -385,11 +414,12 @@ export abstract class QueryClient {
* Execute prepared statements with template strings
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
* const id = 12;
* // Array<{id: number, name: string}>
* const { rows } = await my_client.queryObject<{id: number, name: string}>`SELECT ID, NAME FROM CLIENTS WHERE ID = ${id}`;
+ * await my_client.end();
* ```
*/
async queryObject(
@@ -447,10 +477,10 @@ export abstract class QueryClient {
* statements asynchronously
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* await client.connect();
- * await client.queryArray`UPDATE MY_TABLE SET MY_FIELD = 0`;
+ * await client.queryArray`SELECT * FROM CLIENTS`;
* await client.end();
* ```
*
@@ -458,18 +488,17 @@ export abstract class QueryClient {
* for concurrency capabilities check out connection pools
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client_1 = new Client();
* await client_1.connect();
* // Even if operations are not awaited, they will be executed in the order they were
* // scheduled
- * client_1.queryArray`UPDATE MY_TABLE SET MY_FIELD = 0`;
- * client_1.queryArray`DELETE FROM MY_TABLE`;
+ * client_1.queryArray`DELETE FROM CLIENTS`;
*
* const client_2 = new Client();
* await client_2.connect();
* // `client_2` will execute it's queries in parallel to `client_1`
- * const {rows: result} = await client_2.queryArray`SELECT * FROM MY_TABLE`;
+ * const {rows: result} = await client_2.queryArray`SELECT * FROM CLIENTS`;
*
* await client_1.end();
* await client_2.end();
@@ -515,4 +544,8 @@ export class PoolClient extends QueryClient {
// Cleanup all session related metadata
this.resetSessionMetadata();
}
+
+ [Symbol.dispose]() {
+ this.release();
+ }
}
diff --git a/client/error.ts b/client/error.ts
index 7fc4cccd..fa759980 100644
--- a/client/error.ts
+++ b/client/error.ts
@@ -1,4 +1,4 @@
-import { type Notice } from "../connection/message.ts";
+import type { Notice } from "../connection/message.ts";
/**
* A connection error
@@ -20,7 +20,7 @@ export class ConnectionParamsError extends Error {
/**
* Create a new ConnectionParamsError
*/
- constructor(message: string, cause?: Error) {
+ constructor(message: string, cause?: unknown) {
super(message, { cause });
this.name = "ConnectionParamsError";
}
diff --git a/connection/auth.ts b/connection/auth.ts
index c32e7b88..e77b8830 100644
--- a/connection/auth.ts
+++ b/connection/auth.ts
@@ -1,9 +1,10 @@
-import { crypto, hex } from "../deps.ts";
+import { crypto } from "@std/crypto/crypto";
+import { encodeHex } from "@std/encoding/hex";
const encoder = new TextEncoder();
async function md5(bytes: Uint8Array): Promise {
- return hex.encodeHex(await crypto.subtle.digest("MD5", bytes));
+ return encodeHex(await crypto.subtle.digest("MD5", bytes));
}
// AuthenticationMD5Password
diff --git a/connection/connection.ts b/connection/connection.ts
index 7ce3d38d..9c0e66a2 100644
--- a/connection/connection.ts
+++ b/connection/connection.ts
@@ -26,15 +26,8 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import {
- bold,
- BufReader,
- BufWriter,
- delay,
- joinPath,
- rgb24,
- yellow,
-} from "../deps.ts";
+import { join as joinPath } from "@std/path";
+import { bold, rgb24, yellow } from "@std/fmt/colors";
import { DeferredStack } from "../utils/deferred.ts";
import { getSocketName, readUInt32BE } from "../utils/utils.ts";
import { PacketWriter } from "./packet.ts";
@@ -54,7 +47,7 @@ import {
type QueryResult,
ResultType,
} from "../query/query.ts";
-import { type ClientConfiguration } from "./connection_params.ts";
+import type { ClientConfiguration } from "./connection_params.ts";
import * as scram from "./scram.ts";
import {
ConnectionError,
@@ -127,8 +120,6 @@ const encoder = new TextEncoder();
// - Refactor properties to not be lazily initialized
// or to handle their undefined value
export class Connection {
- #bufReader!: BufReader;
- #bufWriter!: BufWriter;
#conn!: Deno.Conn;
connected = false;
#connection_params: ClientConfiguration;
@@ -142,6 +133,7 @@ export class Connection {
#secretKey?: number;
#tls?: boolean;
#transport?: "tcp" | "socket";
+ #connWritable!: WritableStreamDefaultWriter;
get pid(): number | undefined {
return this.#pid;
@@ -165,13 +157,39 @@ export class Connection {
this.#onDisconnection = disconnection_callback;
}
+ /**
+ * Read p.length bytes into the buffer
+ */
+ async #readFull(p: Uint8Array): Promise {
+ let bytes_read = 0;
+ while (bytes_read < p.length) {
+ try {
+ const read_result = await this.#conn.read(p.subarray(bytes_read));
+ if (read_result === null) {
+ if (bytes_read === 0) {
+ return;
+ } else {
+ throw new ConnectionError("Failed to read bytes from socket");
+ }
+ }
+ bytes_read += read_result;
+ } catch (e) {
+ if (e instanceof Deno.errors.ConnectionReset) {
+ throw new ConnectionError("The session was terminated unexpectedly");
+ }
+ throw e;
+ }
+ }
+ }
+
/**
* Read single message sent by backend
*/
async #readMessage(): Promise {
// Clear buffer before reading the message type
this.#message_header.fill(0);
- await this.#bufReader.readFull(this.#message_header);
+ await this.#readFull(this.#message_header);
+
const type = decoder.decode(this.#message_header.slice(0, 1));
// TODO
// Investigate if the ascii terminator is the best way to check for a broken
@@ -187,7 +205,7 @@ export class Connection {
}
const length = readUInt32BE(this.#message_header, 1) - 4;
const body = new Uint8Array(length);
- await this.#bufReader.readFull(body);
+ await this.#readFull(body);
return new Message(type, length, body);
}
@@ -197,8 +215,7 @@ export class Connection {
writer.clear();
writer.addInt32(8).addInt32(80877103).join();
- await this.#bufWriter.write(writer.flush());
- await this.#bufWriter.flush();
+ await this.#connWritable.write(writer.flush());
const response = new Uint8Array(1);
await this.#conn.read(response);
@@ -254,8 +271,7 @@ export class Connection {
const finalBuffer = writer.addInt32(bodyLength).add(bodyBuffer).join();
- await this.#bufWriter.write(finalBuffer);
- await this.#bufWriter.flush();
+ await this.#connWritable.write(finalBuffer);
return await this.#readMessage();
}
@@ -264,8 +280,7 @@ export class Connection {
// @ts-expect-error This will throw in runtime if the options passed to it are socket related and deno is running
// on stable
this.#conn = await Deno.connect(options);
- this.#bufWriter = new BufWriter(this.#conn);
- this.#bufReader = new BufReader(this.#conn);
+ this.#connWritable = this.#conn.writable.getWriter();
}
async #openSocketConnection(path: string, port: number) {
@@ -295,12 +310,11 @@ export class Connection {
}
async #openTlsConnection(
- connection: Deno.Conn,
+ connection: Deno.TcpConn,
options: { hostname: string; caCerts: string[] },
) {
this.#conn = await Deno.startTls(connection, options);
- this.#bufWriter = new BufWriter(this.#conn);
- this.#bufReader = new BufReader(this.#conn);
+ this.#connWritable = this.#conn.writable.getWriter();
}
#resetConnectionMetadata() {
@@ -338,7 +352,7 @@ export class Connection {
this.#tls = undefined;
this.#transport = "socket";
} else {
- // A BufWriter needs to be available in order to check if the server accepts TLS connections
+ // A writer needs to be available in order to check if the server accepts TLS connections
await this.#openConnection({ hostname, port, transport: "tcp" });
this.#tls = false;
this.#transport = "tcp";
@@ -354,7 +368,9 @@ export class Connection {
// https://www.postgresql.org/docs/14/protocol-flow.html#id-1.10.5.7.11
if (accepts_tls) {
try {
- await this.#openTlsConnection(this.#conn, {
+ // TODO: handle connection type without castinggaa
+ // https://github.com/denoland/deno/issues/10200
+ await this.#openTlsConnection(this.#conn as Deno.TcpConn, {
hostname,
caCerts: caCertificates,
});
@@ -363,7 +379,7 @@ export class Connection {
if (!tls_enforced) {
console.error(
bold(yellow("TLS connection failed with message: ")) +
- e.message +
+ (e instanceof Error ? e.message : e) +
"\n" +
bold("Defaulting to non-encrypted connection"),
);
@@ -390,10 +406,14 @@ export class Connection {
} catch (e) {
// Make sure to close the connection before erroring or reseting
this.#closeConnection();
- if (e instanceof Deno.errors.InvalidData && tls_enabled) {
+ if (
+ (e instanceof Deno.errors.InvalidData ||
+ e instanceof Deno.errors.BadResource) && tls_enabled
+ ) {
if (tls_enforced) {
throw new Error(
- "The certificate used to secure the TLS connection is invalid.",
+ "The certificate used to secure the TLS connection is invalid: " +
+ e.message,
);
} else {
console.error(
@@ -431,6 +451,8 @@ export class Connection {
}
case INCOMING_AUTHENTICATION_MESSAGES.PARAMETER_STATUS:
break;
+ case INCOMING_AUTHENTICATION_MESSAGES.NOTICE:
+ break;
default:
throw new Error(`Unknown response for startup: ${message.type}`);
}
@@ -463,7 +485,7 @@ export class Connection {
let reconnection_attempts = 0;
const max_reconnections = this.#connection_params.connection.attempts;
- let error: Error | undefined;
+ let error: unknown | undefined;
// If no connection has been established and the reconnection attempts are
// set to zero, attempt to connect at least once
if (!is_reconnection && this.#connection_params.connection.attempts === 0) {
@@ -487,7 +509,7 @@ export class Connection {
}
if (interval > 0) {
- await delay(interval);
+ await new Promise((resolve) => setTimeout(resolve, interval));
}
}
try {
@@ -561,8 +583,7 @@ export class Connection {
const password = this.#connection_params.password || "";
const buffer = this.#packetWriter.addCString(password).flush(0x70);
- await this.#bufWriter.write(buffer);
- await this.#bufWriter.flush();
+ await this.#connWritable.write(buffer);
return this.#readMessage();
}
@@ -583,8 +604,7 @@ export class Connection {
);
const buffer = this.#packetWriter.addCString(password).flush(0x70);
- await this.#bufWriter.write(buffer);
- await this.#bufWriter.flush();
+ await this.#connWritable.write(buffer);
return this.#readMessage();
}
@@ -611,8 +631,7 @@ export class Connection {
this.#packetWriter.addCString("SCRAM-SHA-256");
this.#packetWriter.addInt32(clientFirstMessage.length);
this.#packetWriter.addString(clientFirstMessage);
- this.#bufWriter.write(this.#packetWriter.flush(0x70));
- this.#bufWriter.flush();
+ this.#connWritable.write(this.#packetWriter.flush(0x70));
const maybe_sasl_continue = await this.#readMessage();
switch (maybe_sasl_continue.type) {
@@ -639,8 +658,7 @@ export class Connection {
this.#packetWriter.clear();
this.#packetWriter.addString(await client.composeResponse());
- this.#bufWriter.write(this.#packetWriter.flush(0x70));
- this.#bufWriter.flush();
+ this.#connWritable.write(this.#packetWriter.flush(0x70));
const maybe_sasl_final = await this.#readMessage();
switch (maybe_sasl_final.type) {
@@ -676,8 +694,7 @@ export class Connection {
const buffer = this.#packetWriter.addCString(query.text).flush(0x51);
- await this.#bufWriter.write(buffer);
- await this.#bufWriter.flush();
+ await this.#connWritable.write(buffer);
let result;
if (query.result_type === ResultType.ARRAY) {
@@ -686,7 +703,7 @@ export class Connection {
result = new QueryObjectResult(query);
}
- let error: Error | undefined;
+ let error: unknown | undefined;
let current_message = await this.#readMessage();
// Process messages until ready signal is sent
@@ -766,7 +783,7 @@ export class Connection {
.addCString(query.text)
.addInt16(0)
.flush(0x50);
- await this.#bufWriter.write(buffer);
+ await this.#connWritable.write(buffer);
}
async #appendArgumentsToMessage(query: Query) {
@@ -783,16 +800,16 @@ export class Connection {
if (hasBinaryArgs) {
this.#packetWriter.addInt16(query.args.length);
- query.args.forEach((arg) => {
+ for (const arg of query.args) {
this.#packetWriter.addInt16(arg instanceof Uint8Array ? 1 : 0);
- });
+ }
} else {
this.#packetWriter.addInt16(0);
}
this.#packetWriter.addInt16(query.args.length);
- query.args.forEach((arg) => {
+ for (const arg of query.args) {
if (arg === null || typeof arg === "undefined") {
this.#packetWriter.addInt32(-1);
} else if (arg instanceof Uint8Array) {
@@ -803,11 +820,11 @@ export class Connection {
this.#packetWriter.addInt32(byteLength);
this.#packetWriter.addString(arg);
}
- });
+ }
this.#packetWriter.addInt16(0);
const buffer = this.#packetWriter.flush(0x42);
- await this.#bufWriter.write(buffer);
+ await this.#connWritable.write(buffer);
}
/**
@@ -818,7 +835,7 @@ export class Connection {
this.#packetWriter.clear();
const buffer = this.#packetWriter.addCString("P").flush(0x44);
- await this.#bufWriter.write(buffer);
+ await this.#connWritable.write(buffer);
}
async #appendExecuteToMessage() {
@@ -828,14 +845,14 @@ export class Connection {
.addCString("") // unnamed portal
.addInt32(0)
.flush(0x45);
- await this.#bufWriter.write(buffer);
+ await this.#connWritable.write(buffer);
}
async #appendSyncToMessage() {
this.#packetWriter.clear();
const buffer = this.#packetWriter.flush(0x53);
- await this.#bufWriter.write(buffer);
+ await this.#connWritable.write(buffer);
}
// TODO
@@ -873,8 +890,6 @@ export class Connection {
// The execute response contains the portal in which the query will be run and how many rows should it return
await this.#appendExecuteToMessage();
await this.#appendSyncToMessage();
- // send all messages to backend
- await this.#bufWriter.flush();
let result;
if (query.result_type === ResultType.ARRAY) {
@@ -883,7 +898,7 @@ export class Connection {
result = new QueryObjectResult(query);
}
- let error: Error | undefined;
+ let error: unknown | undefined;
let current_message = await this.#readMessage();
while (current_message.type !== INCOMING_QUERY_MESSAGES.READY) {
@@ -997,9 +1012,9 @@ export class Connection {
async end(): Promise {
if (this.connected) {
const terminationMessage = new Uint8Array([0x58, 0x00, 0x00, 0x00, 0x04]);
- await this.#bufWriter.write(terminationMessage);
+ await this.#connWritable.write(terminationMessage);
try {
- await this.#bufWriter.flush();
+ await this.#connWritable.ready;
} catch (_e) {
// This steps can fail if the underlying connection was closed ungracefully
} finally {
diff --git a/connection/connection_params.ts b/connection/connection_params.ts
index 7b68ea9c..a55fb804 100644
--- a/connection/connection_params.ts
+++ b/connection/connection_params.ts
@@ -1,8 +1,9 @@
import { parseConnectionUri } from "../utils/utils.ts";
import { ConnectionParamsError } from "../client/error.ts";
-import { fromFileUrl, isAbsolute } from "../deps.ts";
-import { OidType } from "../query/oid.ts";
-import { DebugControls } from "../debug.ts";
+import { fromFileUrl, isAbsolute } from "@std/path";
+import type { OidType } from "../query/oid.ts";
+import type { DebugControls } from "../debug.ts";
+import type { ParseArrayFunction } from "../query/array_parser.ts";
/**
* The connection string must match the following URI structure. All parameters but database and user are optional
@@ -108,9 +109,16 @@ export type Decoders = {
/**
* A decoder function that takes a string value and returns a parsed value of some type.
- * the Oid is also passed to the function for reference
+ *
+ * @param value The string value to parse
+ * @param oid The OID of the column type the value is from
+ * @param parseArray A helper function that parses SQL array-formatted strings and parses each array value using a transform function.
*/
-export type DecoderFunction = (value: string, oid: number) => unknown;
+export type DecoderFunction = (
+ value: string,
+ oid: number,
+ parseArray: ParseArrayFunction,
+) => unknown;
/**
* Control the behavior for the client instance
@@ -141,15 +149,14 @@ export type ClientControls = {
*
* @example
* ```ts
- * import dayjs from 'https://esm.sh/dayjs';
- * import { Oid,Decoders } from '../mod.ts'
+ * import { Oid, Decoders } from '../mod.ts'
*
* {
* const decoders: Decoders = {
* // 16 = Oid.bool : convert all boolean values to numbers
* '16': (value: string) => value === 't' ? 1 : 0,
- * // 1082 = Oid.date : convert all dates to dayjs objects
- * 1082: (value: string) => dayjs(value),
+ * // 1082 = Oid.date : convert all dates to Date objects
+ * 1082: (value: string) => new Date(value),
* // 23 = Oid.int4 : convert all integers to positive numbers
* [Oid.int4]: (value: string) => Math.max(0, parseInt(value || '0', 10)),
* }
@@ -412,7 +419,15 @@ export function createParams(
try {
pgEnv = getPgEnv();
} catch (e) {
- if (e instanceof Deno.errors.PermissionDenied) {
+ // In Deno v1, Deno permission errors resulted in a Deno.errors.PermissionDenied exception. In Deno v2, a new
+ // Deno.errors.NotCapable exception was added to replace this. The "in" check makes this code safe for both Deno
+ // 1 and Deno 2
+ if (
+ e instanceof
+ ("NotCapable" in Deno.errors
+ ? Deno.errors.NotCapable
+ : Deno.errors.PermissionDenied)
+ ) {
has_env_access = false;
} else {
throw e;
diff --git a/connection/message_code.ts b/connection/message_code.ts
index ede4ed09..979fc1a3 100644
--- a/connection/message_code.ts
+++ b/connection/message_code.ts
@@ -24,6 +24,7 @@ export const INCOMING_AUTHENTICATION_MESSAGES = {
BACKEND_KEY: "K",
PARAMETER_STATUS: "S",
READY: "Z",
+ NOTICE: "N",
} as const;
export const INCOMING_TLS_MESSAGES = {
diff --git a/connection/packet.ts b/connection/packet.ts
index 36abae18..2d93f695 100644
--- a/connection/packet.ts
+++ b/connection/packet.ts
@@ -25,7 +25,7 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import { copy } from "../deps.ts";
+import { copy } from "@std/bytes/copy";
import { readInt16BE, readInt32BE } from "../utils/utils.ts";
export class PacketReader {
diff --git a/connection/scram.ts b/connection/scram.ts
index 1ef2661e..e4e18c32 100644
--- a/connection/scram.ts
+++ b/connection/scram.ts
@@ -1,4 +1,4 @@
-import { base64 } from "../deps.ts";
+import { decodeBase64, encodeBase64 } from "@std/encoding/base64";
/** Number of random bytes used to generate a nonce */
const defaultNonceSize = 16;
@@ -132,7 +132,7 @@ function escape(str: string): string {
}
function generateRandomNonce(size: number): string {
- return base64.encodeBase64(crypto.getRandomValues(new Uint8Array(size)));
+ return encodeBase64(crypto.getRandomValues(new Uint8Array(size)));
}
function parseScramAttributes(message: string): Record {
@@ -144,10 +144,8 @@ function parseScramAttributes(message: string): Record {
throw new Error(Reason.BadMessage);
}
- // TODO
- // Replace with String.prototype.substring
- const key = entry.substr(0, pos);
- const value = entry.substr(pos + 1);
+ const key = entry.substring(0, pos);
+ const value = entry.slice(pos + 1);
attrs[key] = value;
}
@@ -221,7 +219,7 @@ export class Client {
throw new Error(Reason.BadSalt);
}
try {
- salt = base64.decodeBase64(attrs.s);
+ salt = decodeBase64(attrs.s);
} catch {
throw new Error(Reason.BadSalt);
}
@@ -261,7 +259,7 @@ export class Client {
this.#auth_message += "," + responseWithoutProof;
- const proof = base64.encodeBase64(
+ const proof = encodeBase64(
computeScramProof(
await computeScramSignature(
this.#auth_message,
@@ -294,7 +292,7 @@ export class Client {
throw new Error(attrs.e ?? Reason.Rejected);
}
- const verifier = base64.encodeBase64(
+ const verifier = encodeBase64(
await computeScramSignature(
this.#auth_message,
this.#key_signatures.server,
diff --git a/deno.json b/deno.json
index 51a2bcf8..35e10847 100644
--- a/deno.json
+++ b/deno.json
@@ -1,6 +1,14 @@
{
- "lock": false,
- "name": "@bartlomieju/postgres",
- "version": "0.19.0",
- "exports": "./mod.ts"
+ "name": "@db/postgres",
+ "version": "0.19.5",
+ "license": "MIT",
+ "exports": "./mod.ts",
+ "imports": {
+ "@std/bytes": "jsr:@std/bytes@^1.0.5",
+ "@std/crypto": "jsr:@std/crypto@^1.0.4",
+ "@std/encoding": "jsr:@std/encoding@^1.0.9",
+ "@std/fmt": "jsr:@std/fmt@^1.0.6",
+ "@std/path": "jsr:@std/path@^1.0.8"
+ },
+ "lock": false
}
diff --git a/deps.ts b/deps.ts
deleted file mode 100644
index 3d10e31c..00000000
--- a/deps.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export * as base64 from "https://deno.land/std@0.214.0/encoding/base64.ts";
-export * as hex from "https://deno.land/std@0.214.0/encoding/hex.ts";
-export { parse as parseDate } from "https://deno.land/std@0.214.0/datetime/parse.ts";
-export { BufReader } from "https://deno.land/std@0.214.0/io/buf_reader.ts";
-export { BufWriter } from "https://deno.land/std@0.214.0/io/buf_writer.ts";
-export { copy } from "https://deno.land/std@0.214.0/bytes/copy.ts";
-export { crypto } from "https://deno.land/std@0.214.0/crypto/crypto.ts";
-export { delay } from "https://deno.land/std@0.214.0/async/delay.ts";
-export {
- bold,
- rgb24,
- yellow,
-} from "https://deno.land/std@0.214.0/fmt/colors.ts";
-export {
- fromFileUrl,
- isAbsolute,
- join as joinPath,
-} from "https://deno.land/std@0.214.0/path/mod.ts";
diff --git a/docker-compose.yml b/docker-compose.yml
index be919039..a665103d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
x-database-env:
&database-env
POSTGRES_DB: "postgres"
@@ -11,7 +9,7 @@ x-test-env:
WAIT_HOSTS: "postgres_clear:6000,postgres_md5:6001,postgres_scram:6002"
# Wait fifteen seconds after database goes online
# for database metadata initialization
- WAIT_AFTER_HOSTS: "15"
+ WAIT_AFTER: "15"
x-test-volumes:
&test-volumes
@@ -81,3 +79,19 @@ services:
<<: *test-env
NO_COLOR: "true"
volumes: *test-volumes
+
+ doc_tests:
+ image: postgres/tests
+ command: sh -c "/wait && deno test -A --doc client.ts mod.ts pool.ts client/ connection/ query/ utils/"
+ depends_on:
+ - postgres_clear
+ - postgres_md5
+ - postgres_scram
+ environment:
+ <<: *test-env
+ PGDATABASE: "postgres"
+ PGPASSWORD: "postgres"
+ PGUSER: "postgres"
+ PGHOST: "postgres_md5"
+ PGPORT: 6001
+ volumes: *test-volumes
diff --git a/docs/README.md b/docs/README.md
index 477b86f4..97527885 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,17 +1,19 @@
# deno-postgres
-
-[](https://discord.gg/HEdTCvZUSf)
-
-[](https://doc.deno.land/https/deno.land/x/postgres/mod.ts)
-
+
+[](https://discord.com/invite/HEdTCvZUSf)
+[](https://jsr.io/@db/postgres)
+[](https://jsr.io/@db/postgres)
+[](https://deno-postgres.com)
+[](https://jsr.io/@db/postgres/doc)
+[](LICENSE)
`deno-postgres` is a lightweight PostgreSQL driver for Deno focused on user
experience. It provides abstractions for most common operations such as typed
queries, prepared statements, connection pools, and transactions.
```ts
-import { Client } from "https://deno.land/x/postgres/mod.ts";
+import { Client } from "jsr:@db/postgres";
const client = new Client({
user: "user",
@@ -38,7 +40,7 @@ All `deno-postgres` clients provide the following options to authenticate and
manage your connections
```ts
-import { Client } from "https://deno.land/x/postgres/mod.ts";
+import { Client } from "jsr:@db/postgres";
let config;
@@ -114,7 +116,7 @@ of search parameters such as the following:
- password: If password is not specified in the url, this will be taken instead
- port: If port is not specified in the url, this will be taken instead
- options: This parameter can be used by other database engines usable through
- the Postgres protocol (such as Cockroachdb for example) to send additional
+ the Postgres protocol (such as CockroachDB for example) to send additional
values for connection (ej: options=--cluster=your_cluster_name)
- sslmode: Allows you to specify the tls configuration for your client; the
allowed values are the following:
@@ -231,9 +233,6 @@ instead of TCP by providing the route to the socket file your Postgres database
creates automatically. You can manually set the protocol used with the
`host_type` property in the client options
-**Note**: This functionality is only available on UNIX systems under the
-`--unstable` flag
-
In order to connect to the socket you can pass the path as a host in the client
initialization. Alternatively, you can specify the port the database is
listening on and the parent folder of the socket as a host (The equivalent of
@@ -343,8 +342,8 @@ certificate to encrypt your connection that, if not taken care of, can render
your certificate invalid.
When using a self-signed certificate, make sure to specify the PEM encoded CA
-certificate using the `--cert` option when starting Deno (Deno 1.12.2 or later)
-or in the `tls.caCertificates` option when creating a client (Deno 1.15.0 later)
+certificate using the `--cert` option when starting Deno or in the
+`tls.caCertificates` option when creating a client
```ts
const client = new Client({
@@ -381,7 +380,7 @@ https://www.postgresql.org/docs/14/libpq-envars.html)
```ts
// PGUSER=user PGPASSWORD=admin PGDATABASE=test deno run --allow-net --allow-env database.js
-import { Client } from "https://deno.land/x/postgres/mod.ts";
+import { Client } from "jsr:@db/postgres";
const client = new Client();
await client.connect();
@@ -450,9 +449,12 @@ const dbPool = new Pool(
POOL_CONNECTIONS,
);
-const client = await dbPool.connect(); // 19 connections are still available
-await client.queryArray`UPDATE X SET Y = 'Z'`;
-client.release(); // This connection is now available for use again
+// Note the `using` keyword in block scope
+{
+ using client = await dbPool.connect();
+ // 19 connections are still available
+ await client.queryArray`UPDATE X SET Y = 'Z'`;
+} // This connection is now available for use again
```
The number of pools is up to you, but a pool of 20 is good for small
@@ -515,9 +517,9 @@ await client_3.release();
#### Pools made simple
-The following example is a simple abstraction over pools that allows you to
-execute one query and release the used client after returning the result in a
-single function call
+Because of `using` keyword there is no need for manually releasing pool client.
+
+Legacy code like this
```ts
async function runQuery(query: string) {
@@ -532,7 +534,27 @@ async function runQuery(query: string) {
}
await runQuery("SELECT ID, NAME FROM USERS"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
-await runQuery("SELECT ID, NAME FROM USERS WHERE ID = '1'"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
+await runQuery("SELECT ID, NAME FROM USERS WHERE ID = '1'"); // [{id: 1, name: 'Carlos'}]
+```
+
+Can now be written simply as
+
+```ts
+async function runQuery(query: string) {
+ using client = await pool.connect();
+ return await client.queryObject(query);
+}
+
+await runQuery("SELECT ID, NAME FROM USERS"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
+await runQuery("SELECT ID, NAME FROM USERS WHERE ID = '1'"); // [{id: 1, name: 'Carlos'}]
+```
+
+But you can release pool client manually if you wish
+
+```ts
+const client = await dbPool.connect(); // note the `const` instead of `using` keyword
+await client.queryArray`UPDATE X SET Y = 'Z'`;
+client.release(); // This connection is now available for use again
```
## Executing queries
@@ -758,10 +780,10 @@ available:
You can also provide custom decoders to the client that will be used to decode
the result data. This can be done by setting the `decoders` controls option in
the client configuration. This option is a map object where the keys are the
-type names or Oid numbers and the values are the custom decoder functions.
+type names or OID numbers and the values are the custom decoder functions.
You can use it with the decode strategy. Custom decoders take precedence over
-the strategy and internal parsers.
+the strategy and internal decoders.
```ts
{
@@ -785,7 +807,37 @@ the strategy and internal parsers.
const result = await client.queryObject(
"SELECT ID, NAME, IS_ACTIVE FROM PEOPLE",
);
- console.log(result.rows[0]); // {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}}
+ console.log(result.rows[0]);
+ // {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}}
+}
+```
+
+The driver takes care of parsing the related `array` OID types automatically.
+For example, if a custom decoder is defined for the `int4` type, it will be
+applied when parsing `int4[]` arrays. If needed, you can have separate custom
+decoders for the array and non-array types by defining another custom decoders
+for the array type itself.
+
+```ts
+{
+ const client = new Client({
+ database: "some_db",
+ user: "some_user",
+ controls: {
+ decodeStrategy: "string",
+ decoders: {
+ // Custom decoder for int4 (OID 23 = int4)
+ // convert to int and multiply by 100
+ 23: (value: string) => parseInt(value, 10) * 100,
+ },
+ },
+ });
+
+ const result = await client.queryObject(
+ "SELECT ARRAY[ 2, 2, 3, 1 ] AS scores, 8 final_score;",
+ );
+ console.log(result.rows[0]);
+ // { scores: [ 200, 200, 300, 100 ], final_score: 800 }
}
```
@@ -1074,16 +1126,16 @@ const transaction = client.createTransaction("abortable");
await transaction.begin();
let savepoint;
-try{
+try {
// Oops, savepoints can't start with a number
// Validation error, transaction won't be ended
savepoint = await transaction.savepoint("1");
-}catch(e){
+} catch (e) {
// We validate the error was not related to transaction execution
- if(!(e instance of TransactionError)){
+ if (!(e instanceof TransactionError)) {
// We create a good savepoint we can use
savepoint = await transaction.savepoint("a_valid_name");
- }else{
+ } else {
throw e;
}
}
@@ -1399,7 +1451,7 @@ await transaction.commit();
The driver can provide different types of logs if as needed. By default, logs
are disabled to keep your environment as uncluttered as possible. Logging can be
enabled by using the `debug` option in the Client `controls` parameter. Pass
-`true` to enable all logs, or turn on logs granulary by enabling the following
+`true` to enable all logs, or turn on logs granularity by enabling the following
options:
- `queries` : Logs all SQL queries executed by the client
@@ -1412,7 +1464,7 @@ options:
```ts
// debug_test.ts
-import { Client } from "./mod.ts";
+import { Client } from "jsr:@db/postgres";
const client = new Client({
user: "postgres",
@@ -1444,7 +1496,7 @@ AS $function$
BEGIN
RAISE INFO 'This function generates a random UUID :)';
RAISE NOTICE 'A UUID takes up 128 bits in memory.';
- RAISE WARNING 'UUIDs must follow a specific format and lenght in order to be valid!';
+ RAISE WARNING 'UUIDs must follow a specific format and length in order to be valid!';
RETURN gen_random_uuid();
END;
$function$;;
diff --git a/docs/deno-postgres.png b/docs/deno-postgres.png
new file mode 100644
index 00000000..3c1e735d
Binary files /dev/null and b/docs/deno-postgres.png differ
diff --git a/docs/index.html b/docs/index.html
index 4ce33e9f..2fc96d36 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,22 +1,31 @@
-
-
- Deno Postgres
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Deno Postgres
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mod.ts b/mod.ts
index be4ee055..13499468 100644
--- a/mod.ts
+++ b/mod.ts
@@ -5,12 +5,7 @@ export {
TransactionError,
} from "./client/error.ts";
export { Pool } from "./pool.ts";
-export { Oid, OidTypes } from "./query/oid.ts";
-
-// TODO
-// Remove the following reexports after https://doc.deno.land
-// supports two level depth exports
-export type { OidType, OidValue } from "./query/oid.ts";
+export { Oid, type OidType, OidTypes, type OidValue } from "./query/oid.ts";
export type {
ClientOptions,
ConnectionOptions,
diff --git a/pool.ts b/pool.ts
index ae2b58e6..16713d53 100644
--- a/pool.ts
+++ b/pool.ts
@@ -14,18 +14,19 @@ import { DeferredAccessStack } from "./utils/deferred.ts";
* with their PostgreSQL database
*
* ```ts
- * import { Pool } from "https://deno.land/x/postgres/mod.ts";
+ * import { Pool } from "jsr:@db/postgres";
* const pool = new Pool({
- * database: "database",
- * hostname: "hostname",
- * password: "password",
- * port: 5432,
- * user: "user",
+ * database: Deno.env.get("PGDATABASE"),
+ * hostname: Deno.env.get("PGHOST"),
+ * password: Deno.env.get("PGPASSWORD"),
+ * port: Deno.env.get("PGPORT"),
+ * user: Deno.env.get("PGUSER"),
* }, 10); // Creates a pool with 10 available connections
*
* const client = await pool.connect();
* await client.queryArray`SELECT 1`;
* client.release();
+ * await pool.end();
* ```
*
* You can also opt to not initialize all your connections at once by passing the `lazy`
@@ -34,7 +35,7 @@ import { DeferredAccessStack } from "./utils/deferred.ts";
* available connections in the pool
*
* ```ts
- * import { Pool } from "https://deno.land/x/postgres/mod.ts";
+ * import { Pool } from "jsr:@db/postgres";
* // Creates a pool with 10 max available connections
* // Connection with the database won't be established until the user requires it
* const pool = new Pool({}, 10, true);
@@ -53,6 +54,7 @@ import { DeferredAccessStack } from "./utils/deferred.ts";
* const client_3 = await pool.connect();
* client_2.release();
* client_3.release();
+ * await pool.end();
* ```
*/
export class Pool {
@@ -117,11 +119,12 @@ export class Pool {
* with the database if no other connections are available
*
* ```ts
- * import { Pool } from "https://deno.land/x/postgres/mod.ts";
+ * import { Pool } from "jsr:@db/postgres";
* const pool = new Pool({}, 10);
* const client = await pool.connect();
- * await client.queryArray`UPDATE MY_TABLE SET X = 1`;
+ * await client.queryArray`SELECT * FROM CLIENTS`;
* client.release();
+ * await pool.end();
* ```
*/
async connect(): Promise {
@@ -138,24 +141,29 @@ export class Pool {
* This will close all open connections and set a terminated status in the pool
*
* ```ts
- * import { Pool } from "https://deno.land/x/postgres/mod.ts";
+ * import { Pool } from "jsr:@db/postgres";
* const pool = new Pool({}, 10);
*
* await pool.end();
* console.assert(pool.available === 0, "There are connections available after ending the pool");
- * await pool.end(); // An exception will be thrown, pool doesn't have any connections to close
+ * try {
+ * await pool.end(); // An exception will be thrown, pool doesn't have any connections to close
+ * } catch (e) {
+ * console.log(e);
+ * }
* ```
*
* However, a terminated pool can be reused by using the "connect" method, which
* will reinitialize the connections according to the original configuration of the pool
*
* ```ts
- * import { Pool } from "https://deno.land/x/postgres/mod.ts";
+ * import { Pool } from "jsr:@db/postgres";
* const pool = new Pool({}, 10);
* await pool.end();
* const client = await pool.connect();
* await client.queryArray`SELECT 1`; // Works!
* client.release();
+ * await pool.end();
* ```
*/
async end(): Promise {
diff --git a/query/array_parser.ts b/query/array_parser.ts
index 9fd043bd..8ca9175f 100644
--- a/query/array_parser.ts
+++ b/query/array_parser.ts
@@ -6,11 +6,21 @@ type AllowedSeparators = "," | ";";
type ArrayResult = Array>;
type Transformer = (value: string) => T;
+export type ParseArrayFunction = typeof parseArray;
+
+/**
+ * Parse a string into an array of values using the provided transform function.
+ *
+ * @param source The string to parse
+ * @param transform A function to transform each value in the array
+ * @param separator The separator used to split the string into values
+ * @returns
+ */
export function parseArray(
source: string,
transform: Transformer,
separator: AllowedSeparators = ",",
-) {
+): ArrayResult {
return new ArrayParser(source, transform, separator).parse();
}
@@ -79,7 +89,7 @@ class ArrayParser {
this.dimension++;
if (this.dimension > 1) {
parser = new ArrayParser(
- this.source.substr(this.position - 1),
+ this.source.substring(this.position - 1),
this.transform,
this.separator,
);
diff --git a/query/decode.ts b/query/decode.ts
index c2b5ec42..c0311910 100644
--- a/query/decode.ts
+++ b/query/decode.ts
@@ -1,5 +1,5 @@
-import { Oid, OidTypes, OidValue } from "./oid.ts";
-import { bold, yellow } from "../deps.ts";
+import { Oid, type OidType, OidTypes, type OidValue } from "./oid.ts";
+import { bold, yellow } from "@std/fmt/colors";
import {
decodeBigint,
decodeBigintArray,
@@ -35,7 +35,8 @@ import {
decodeTid,
decodeTidArray,
} from "./decoders.ts";
-import { ClientControls } from "../connection/connection_params.ts";
+import type { ClientControls } from "../connection/connection_params.ts";
+import { parseArray } from "./array_parser.ts";
export class Column {
constructor(
@@ -195,10 +196,10 @@ function decodeText(value: string, typeOid: number) {
// them as they see fit
return value;
}
- } catch (_e) {
+ } catch (e) {
console.error(
bold(yellow(`Error decoding type Oid ${typeOid} value`)) +
- _e.message +
+ (e instanceof Error ? e.message : e) +
"\n" +
bold("Defaulting to null."),
);
@@ -216,12 +217,29 @@ export function decode(
// check if there is a custom decoder
if (controls?.decoders) {
+ const oidType = OidTypes[column.typeOid as OidValue];
// check if there is a custom decoder by oid (number) or by type name (string)
const decoderFunc = controls.decoders?.[column.typeOid] ||
- controls.decoders?.[OidTypes[column.typeOid as OidValue]];
+ controls.decoders?.[oidType];
if (decoderFunc) {
- return decoderFunc(strValue, column.typeOid);
+ return decoderFunc(strValue, column.typeOid, parseArray);
+ } // if no custom decoder is found and the oid is for an array type, check if there is
+ // a decoder for the base type and use that with the array parser
+ else if (oidType?.includes("_array")) {
+ const baseOidType = oidType.replace("_array", "") as OidType;
+ // check if the base type is in the Oid object
+ if (baseOidType in Oid) {
+ // check if there is a custom decoder for the base type by oid (number) or by type name (string)
+ const decoderFunc = controls.decoders?.[Oid[baseOidType]] ||
+ controls.decoders?.[baseOidType];
+ if (decoderFunc) {
+ return parseArray(
+ strValue,
+ (value: string) => decoderFunc(value, column.typeOid, parseArray),
+ );
+ }
+ }
}
}
diff --git a/query/decoders.ts b/query/decoders.ts
index 4edbb03a..58356d76 100644
--- a/query/decoders.ts
+++ b/query/decoders.ts
@@ -1,4 +1,3 @@
-import { parseDate } from "../deps.ts";
import { parseArray } from "./array_parser.ts";
import type {
Box,
@@ -64,7 +63,9 @@ export function decodeBox(value: string): Box {
b: decodePoint(b),
};
} catch (e) {
- throw new Error(`Invalid Box: "${value}" : ${e.message}`);
+ throw new Error(
+ `Invalid Box: "${value}" : ${(e instanceof Error ? e.message : e)}`,
+ );
}
}
@@ -93,8 +94,8 @@ function decodeByteaEscape(byteaStr: string): Uint8Array {
bytes.push(byteaStr.charCodeAt(i));
++i;
} else {
- if (/[0-7]{3}/.test(byteaStr.substr(i + 1, 3))) {
- bytes.push(parseInt(byteaStr.substr(i + 1, 3), 8));
+ if (/[0-7]{3}/.test(byteaStr.substring(i + 1, i + 4))) {
+ bytes.push(parseInt(byteaStr.substring(i + 1, i + 4), 8));
i += 4;
} else {
let backslashes = 1;
@@ -140,7 +141,9 @@ export function decodeCircle(value: string): Circle {
radius: radius,
};
} catch (e) {
- throw new Error(`Invalid Circle: "${value}" : ${e.message}`);
+ throw new Error(
+ `Invalid Circle: "${value}" : ${(e instanceof Error ? e.message : e)}`,
+ );
}
}
@@ -157,7 +160,7 @@ export function decodeDate(dateStr: string): Date | number {
return Number(-Infinity);
}
- return parseDate(dateStr, "yyyy-MM-dd");
+ return new Date(dateStr);
}
export function decodeDateArray(value: string) {
@@ -249,13 +252,13 @@ export function decodeLine(value: string): Line {
);
}
- equationConsts.forEach((c) => {
+ for (const c of equationConsts) {
if (Number.isNaN(parseFloat(c))) {
throw new Error(
`Invalid Line: "${value}". Line constant "${c}" must be a valid number.`,
);
}
- });
+ }
const [a, b, c] = equationConsts;
@@ -287,7 +290,11 @@ export function decodeLineSegment(value: string): LineSegment {
b: decodePoint(b),
};
} catch (e) {
- throw new Error(`Invalid Line Segment: "${value}" : ${e.message}`);
+ throw new Error(
+ `Invalid Line Segment: "${value}" : ${(e instanceof Error
+ ? e.message
+ : e)}`,
+ );
}
}
@@ -304,7 +311,9 @@ export function decodePath(value: string): Path {
try {
return decodePoint(point);
} catch (e) {
- throw new Error(`Invalid Path: "${value}" : ${e.message}`);
+ throw new Error(
+ `Invalid Path: "${value}" : ${(e instanceof Error ? e.message : e)}`,
+ );
}
});
}
@@ -348,7 +357,9 @@ export function decodePolygon(value: string): Polygon {
try {
return decodePath(value);
} catch (e) {
- throw new Error(`Invalid Polygon: "${value}" : ${e.message}`);
+ throw new Error(
+ `Invalid Polygon: "${value}" : ${(e instanceof Error ? e.message : e)}`,
+ );
}
}
diff --git a/query/encode.ts b/query/encode.ts
index 36407bf2..94cf2b60 100644
--- a/query/encode.ts
+++ b/query/encode.ts
@@ -50,24 +50,23 @@ function escapeArrayElement(value: unknown): string {
function encodeArray(array: Array): string {
let encodedArray = "{";
- array.forEach((element, index) => {
+ for (let index = 0; index < array.length; index++) {
if (index > 0) {
encodedArray += ",";
}
+ const element = array[index];
if (element === null || typeof element === "undefined") {
encodedArray += "NULL";
} else if (Array.isArray(element)) {
encodedArray += encodeArray(element);
} else if (element instanceof Uint8Array) {
- // TODO
- // Should it be encoded as bytea?
- throw new Error("Can't encode array of buffers.");
+ encodedArray += encodeBytes(element);
} else {
const encodedElement = encodeArgument(element);
encodedArray += escapeArrayElement(encodedElement as string);
}
- });
+ }
encodedArray += "}";
return encodedArray;
@@ -91,15 +90,18 @@ export type EncodedArg = null | string | Uint8Array;
export function encodeArgument(value: unknown): EncodedArg {
if (value === null || typeof value === "undefined") {
return null;
- } else if (value instanceof Uint8Array) {
+ }
+ if (value instanceof Uint8Array) {
return encodeBytes(value);
- } else if (value instanceof Date) {
+ }
+ if (value instanceof Date) {
return encodeDate(value);
- } else if (value instanceof Array) {
+ }
+ if (value instanceof Array) {
return encodeArray(value);
- } else if (value instanceof Object) {
+ }
+ if (value instanceof Object) {
return JSON.stringify(value);
- } else {
- return String(value);
}
+ return String(value);
}
diff --git a/query/query.ts b/query/query.ts
index 58977459..bdf0276e 100644
--- a/query/query.ts
+++ b/query/query.ts
@@ -1,7 +1,7 @@
import { encodeArgument, type EncodedArg } from "./encode.ts";
import { type Column, decode } from "./decode.ts";
-import { type Notice } from "../connection/message.ts";
-import { type ClientControls } from "../connection/connection_params.ts";
+import type { Notice } from "../connection/message.ts";
+import type { ClientControls } from "../connection/connection_params.ts";
// TODO
// Limit the type of parameters that can be passed
@@ -15,13 +15,14 @@ import { type ClientControls } from "../connection/connection_params.ts";
* They will take the position according to the order in which they were provided
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
- * await my_client.queryArray("SELECT ID, NAME FROM PEOPLE WHERE AGE > $1 AND AGE < $2", [
- * 10, // $1
- * 20, // $2
+ * await my_client.queryArray("SELECT ID, NAME FROM CLIENTS WHERE NAME = $1", [
+ * "John", // $1
* ]);
+ *
+ * await my_client.end();
* ```
*/
@@ -38,7 +39,8 @@ export type CommandType =
| "SELECT"
| "MOVE"
| "FETCH"
- | "COPY";
+ | "COPY"
+ | "CREATE";
/** Type of a query result */
export enum ResultType {
@@ -154,7 +156,7 @@ export interface QueryObjectOptions extends QueryOptions {
/**
* This class is used to handle the result of a query
*/
-export class QueryResult {
+export abstract class QueryResult {
/**
* Type of query executed for this result
*/
@@ -224,9 +226,7 @@ export class QueryResult {
*
* This function can throw on validation, so any errors must be handled in the message loop accordingly
*/
- insertRow(_row: Uint8Array[]): void {
- throw new Error("No implementation for insertRow is defined");
- }
+ abstract insertRow(_row: Uint8Array[]): void;
}
/**
diff --git a/query/transaction.ts b/query/transaction.ts
index 3dadd33a..2b8dd6ea 100644
--- a/query/transaction.ts
+++ b/query/transaction.ts
@@ -1,4 +1,4 @@
-import { type QueryClient } from "../client.ts";
+import type { QueryClient } from "../client.ts";
import {
Query,
type QueryArguments,
@@ -60,26 +60,36 @@ export class Savepoint {
* Releasing a savepoint will remove it's last instance in the transaction
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
+ * await transaction.begin();
* const savepoint = await transaction.savepoint("n1");
* await savepoint.release();
- * transaction.rollback(savepoint); // Error, can't rollback because the savepoint was released
+ *
+ * try {
+ * await transaction.rollback(savepoint); // Error, can't rollback because the savepoint was released
+ * } catch (e) {
+ * console.log(e);
+ * }
+ *
+ * await client.end();
* ```
*
* It will also allow you to set the savepoint to the position it had before the last update
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction1");
*
+ * await transaction.begin();
* const savepoint = await transaction.savepoint("n1");
* await savepoint.update();
* await savepoint.release(); // This drops the update of the last statement
- * transaction.rollback(savepoint); // Will rollback to the first instance of the savepoint
+ * await transaction.rollback(savepoint); // Will rollback to the first instance of the savepoint
+ * await client.end();
* ```
*
* This function will throw if there are no savepoint instances to drop
@@ -97,29 +107,33 @@ export class Savepoint {
* Updating a savepoint will update its position in the transaction execution
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction1");
*
- * const my_value = "some value";
+ * await transaction.begin();
*
* const savepoint = await transaction.savepoint("n1");
- * transaction.queryArray`INSERT INTO MY_TABLE (X) VALUES (${my_value})`;
+ * transaction.queryArray`DELETE FROM CLIENTS`;
* await savepoint.update(); // Rolling back will now return you to this point on the transaction
+ * await client.end();
* ```
*
* You can also undo a savepoint update by using the `release` method
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction1");
+ *
+ * await transaction.begin();
*
* const savepoint = await transaction.savepoint("n1");
- * transaction.queryArray`DELETE FROM VERY_IMPORTANT_TABLE`;
+ * transaction.queryArray`DELETE FROM CLIENTS`;
* await savepoint.update(); // Oops, shouldn't have updated the savepoint
* await savepoint.release(); // This will undo the last update and return the savepoint to the first instance
* await transaction.rollback(); // Will rollback before the table was deleted
+ * await client.end();
* ```
*/
async update() {
@@ -197,13 +211,14 @@ export class Transaction {
* The begin method will officially begin the transaction, and it must be called before
* any query or transaction operation is executed in order to lock the session
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction_name");
*
* await transaction.begin(); // Session is locked, transaction operations are now safe
* // Important operations
* await transaction.commit(); // Session is unlocked, external operations can now take place
+ * await client.end();
* ```
* https://www.postgresql.org/docs/14/sql-begin.html
*/
@@ -257,9 +272,8 @@ export class Transaction {
} catch (e) {
if (e instanceof PostgresError) {
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
this.#updateClientLock(this.name);
@@ -273,27 +287,31 @@ export class Transaction {
* current transaction and end the current transaction
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
* await transaction.begin();
* // Important operations
* await transaction.commit(); // Will terminate the transaction and save all changes
+ * await client.end();
* ```
*
* The commit method allows you to specify a "chain" option, that allows you to both commit the current changes and
* start a new with the same transaction parameters in a single statement
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction1");
+ *
+ * await transaction.begin();
*
* // Transaction operations I want to commit
* await transaction.commit({ chain: true }); // All changes are saved, following statements will be executed inside a transaction
- * await transaction.queryArray`DELETE SOMETHING FROM SOMEWHERE`; // Still inside the transaction
+ * await transaction.queryArray`DELETE FROM CLIENTS`; // Still inside the transaction
* await transaction.commit(); // The transaction finishes for good
+ * await client.end();
* ```
*
* https://www.postgresql.org/docs/14/sql-commit.html
@@ -312,9 +330,8 @@ export class Transaction {
} catch (e) {
if (e instanceof PostgresError) {
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
}
@@ -346,14 +363,19 @@ export class Transaction {
* the snapshot state between two transactions
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client_1 = new Client();
* const client_2 = new Client();
* const transaction_1 = client_1.createTransaction("transaction");
*
+ * await transaction_1.begin();
+ *
* const snapshot = await transaction_1.getSnapshot();
* const transaction_2 = client_2.createTransaction("new_transaction", { isolation_level: "repeatable_read", snapshot });
* // transaction_2 now shares the same starting state that transaction_1 had
+ *
+ * await client_1.end();
+ * await client_2.end();
* ```
* https://www.postgresql.org/docs/14/functions-admin.html#FUNCTIONS-SNAPSHOT-SYNCHRONIZATION
*/
@@ -371,36 +393,48 @@ export class Transaction {
* It supports a generic interface in order to type the entries retrieved by the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
+ * await transaction.begin();
+ *
* const {rows} = await transaction.queryArray(
* "SELECT ID, NAME FROM CLIENTS"
* ); // Array
+ *
+ * await client.end();
* ```
*
* You can pass type arguments to the query in order to hint TypeScript what the return value will be
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
+ * await transaction.begin();
+ *
* const { rows } = await transaction.queryArray<[number, string]>(
* "SELECT ID, NAME FROM CLIENTS"
* ); // Array<[number, string]>
+ *
+ * await client.end();
* ```
*
* It also allows you to execute prepared stamements with template strings
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
+ * await transaction.begin();
+ *
* const id = 12;
* // Array<[number, string]>
* const { rows } = await transaction.queryArray<[number, string]>`SELECT ID, NAME FROM CLIENTS WHERE ID = ${id}`;
+ *
+ * await client.end();
* ```
*/
async queryArray>(
@@ -411,12 +445,13 @@ export class Transaction {
* Use the configuration object for more advance options to execute the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
* const { rows } = await my_client.queryArray<[number, string]>({
* text: "SELECT ID, NAME FROM CLIENTS",
* name: "select_clients",
* }); // Array<[number, string]>
+ * await my_client.end();
* ```
*/
async queryArray>(
@@ -426,12 +461,14 @@ export class Transaction {
* Execute prepared statements with template strings
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
* const id = 12;
* // Array<[number, string]>
* const {rows} = await my_client.queryArray<[number, string]>`SELECT ID, NAME FROM CLIENTS WHERE ID = ${id}`;
+ *
+ * await my_client.end();
* ```
*/
async queryArray>(
@@ -467,9 +504,8 @@ export class Transaction {
if (e instanceof PostgresError) {
await this.commit();
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
}
@@ -477,7 +513,7 @@ export class Transaction {
* Executed queries and retrieve the data as object entries. It supports a generic in order to type the entries retrieved by the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
* const { rows: rows1 } = await my_client.queryObject(
@@ -487,6 +523,8 @@ export class Transaction {
* const { rows: rows2 } = await my_client.queryObject<{id: number, name: string}>(
* "SELECT ID, NAME FROM CLIENTS"
* ); // Array<{id: number, name: string}>
+ *
+ * await my_client.end();
* ```
*/
async queryObject(
@@ -497,7 +535,7 @@ export class Transaction {
* Use the configuration object for more advance options to execute the query
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
*
* const { rows: rows1 } = await my_client.queryObject(
@@ -510,6 +548,8 @@ export class Transaction {
* fields: ["personal_id", "complete_name"],
* });
* console.log(rows2); // [{personal_id: 78, complete_name: "Frank"}, {personal_id: 15, complete_name: "Sarah"}]
+ *
+ * await my_client.end();
* ```
*/
async queryObject(
@@ -519,11 +559,12 @@ export class Transaction {
* Execute prepared statements with template strings
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const my_client = new Client();
* const id = 12;
* // Array<{id: number, name: string}>
* const { rows } = await my_client.queryObject<{id: number, name: string}>`SELECT ID, NAME FROM CLIENTS WHERE ID = ${id}`;
+ * await my_client.end();
* ```
*/
async queryObject(
@@ -565,9 +606,8 @@ export class Transaction {
if (e instanceof PostgresError) {
await this.commit();
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
}
@@ -578,12 +618,15 @@ export class Transaction {
* Calling a rollback without arguments will terminate the current transaction and undo all changes.
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
+ * await transaction.begin();
+ *
* // Very very important operations that went very, very wrong
* await transaction.rollback(); // Like nothing ever happened
+ * await client.end();
* ```
*
* https://www.postgresql.org/docs/14/sql-rollback.html
@@ -593,13 +636,15 @@ export class Transaction {
* Savepoints can be used to rollback specific changes part of a transaction.
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction1");
+ *
+ * await transaction.begin();
*
* // Important operations I don't want to rollback
* const savepoint = await transaction.savepoint("before_disaster");
- * await transaction.queryArray`UPDATE MY_TABLE SET X = 0`; // Oops, update without where
+ * await transaction.queryArray`DELETE FROM CLIENTS`; // Oops, deleted the wrong thing
*
* // These are all the same, everything that happened between the savepoint and the rollback gets undone
* await transaction.rollback(savepoint);
@@ -607,6 +652,7 @@ export class Transaction {
* await transaction.rollback({ savepoint: 'before_disaster'})
*
* await transaction.commit(); // Commits all other changes
+ * await client.end();
* ```
*/
async rollback(
@@ -616,14 +662,17 @@ export class Transaction {
* The `chain` option allows you to undo the current transaction and restart it with the same parameters in a single statement
*
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction2");
+ *
+ * await transaction.begin();
*
* // Transaction operations I want to undo
* await transaction.rollback({ chain: true }); // All changes are undone, but the following statements will be executed inside a transaction as well
- * await transaction.queryArray`DELETE SOMETHING FROM SOMEWHERE`; // Still inside the transaction
+ * await transaction.queryArray`DELETE FROM CLIENTS`; // Still inside the transaction
* await transaction.commit(); // The transaction finishes for good
+ * await client.end();
* ```
*/
async rollback(options?: { chain?: boolean }): Promise;
@@ -701,9 +750,8 @@ export class Transaction {
if (e instanceof PostgresError) {
await this.commit();
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
this.#resetTransaction();
@@ -725,42 +773,51 @@ export class Transaction {
*
* A savepoint can be easily created like this
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
* const transaction = client.createTransaction("transaction");
*
+ * await transaction.begin();
+ *
* const savepoint = await transaction.savepoint("MY_savepoint"); // returns a `Savepoint` with name "my_savepoint"
* await transaction.rollback(savepoint);
* await savepoint.release(); // The savepoint will be removed
+ * await client.end();
* ```
* All savepoints can have multiple positions in a transaction, and you can change or update
* this positions by using the `update` and `release` methods
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction1");
+ *
+ * await transaction.begin();
*
* const savepoint = await transaction.savepoint("n1");
- * await transaction.queryArray`INSERT INTO MY_TABLE VALUES (${'A'}, ${2})`;
+ * await transaction.queryArray`DELETE FROM CLIENTS`;
* await savepoint.update(); // The savepoint will continue from here
- * await transaction.queryArray`DELETE FROM MY_TABLE`;
- * await transaction.rollback(savepoint); // The transaction will rollback before the delete, but after the insert
+ * await transaction.queryArray`DELETE FROM CLIENTS`;
+ * await transaction.rollback(savepoint); // The transaction will rollback before the secpmd delete
* await savepoint.release(); // The last savepoint will be removed, the original one will remain
- * await transaction.rollback(savepoint); // It rolls back before the insert
+ * await transaction.rollback(savepoint); // It rolls back before the delete
* await savepoint.release(); // All savepoints are released
+ * await client.end();
* ```
*
* Creating a new savepoint with an already used name will return you a reference to
* the original savepoint
* ```ts
- * import { Client } from "https://deno.land/x/postgres/mod.ts";
+ * import { Client } from "jsr:@db/postgres";
* const client = new Client();
- * const transaction = client.createTransaction("transaction");
+ * const transaction = client.createTransaction("transaction2");
+ *
+ * await transaction.begin();
*
* const savepoint_a = await transaction.savepoint("a");
- * await transaction.queryArray`DELETE FROM MY_TABLE`;
+ * await transaction.queryArray`DELETE FROM CLIENTS`;
* const savepoint_b = await transaction.savepoint("a"); // They will be the same savepoint, but the savepoint will be updated to this position
* await transaction.rollback(savepoint_a); // Rolls back to savepoint_b
+ * await client.end();
* ```
* https://www.postgresql.org/docs/14/sql-savepoint.html
*/
@@ -792,9 +849,8 @@ export class Transaction {
if (e instanceof PostgresError) {
await this.commit();
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
} else {
savepoint = new Savepoint(
@@ -813,9 +869,8 @@ export class Transaction {
if (e instanceof PostgresError) {
await this.commit();
throw new TransactionError(this.name, e);
- } else {
- throw e;
}
+ throw e;
}
this.#savepoints.push(savepoint);
}
diff --git a/tests/README.md b/tests/README.md
index c17f1a58..38cc8c41 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,9 +1,11 @@
# Testing
-To run tests, first prepare your configuration file by copying
+To run tests, we recommend using Docker. With Docker, there is no need to modify
+any configuration, just run the build and test commands.
+
+If running tests on your host, prepare your configuration file by copying
`config.example.json` into `config.json` and updating it appropriately based on
-your environment. If you use the Docker based configuration below there's no
-need to modify the configuration.
+your environment.
## Running the Tests
@@ -14,8 +16,8 @@ From within the project directory, run:
deno test --allow-read --allow-net --allow-env
# run in docker container
-docker-compose build --no-cache
-docker-compose run tests
+docker compose build --no-cache
+docker compose run tests
```
## Docker Configuration
@@ -23,7 +25,7 @@ docker-compose run tests
If you have Docker installed then you can run the following to set up a running
container that is compatible with the tests:
-```
+```sh
docker run --rm --env POSTGRES_USER=test --env POSTGRES_PASSWORD=test \
--env POSTGRES_DB=deno_postgres -p 5432:5432 postgres:12-alpine
```
diff --git a/tests/auth_test.ts b/tests/auth_test.ts
index f7ed38db..4b06120e 100644
--- a/tests/auth_test.ts
+++ b/tests/auth_test.ts
@@ -1,4 +1,8 @@
-import { assertEquals, assertNotEquals, assertRejects } from "./test_deps.ts";
+import {
+ assertEquals,
+ assertNotEquals,
+ assertRejects,
+} from "jsr:@std/assert@1.0.10";
import { Client as ScramClient, Reason } from "../connection/scram.ts";
Deno.test("Scram client reproduces RFC 7677 example", async () => {
diff --git a/tests/config.ts b/tests/config.ts
index 17bf701c..0fb0507a 100644
--- a/tests/config.ts
+++ b/tests/config.ts
@@ -1,4 +1,4 @@
-import {
+import type {
ClientConfiguration,
ClientOptions,
} from "../connection/connection_params.ts";
@@ -15,7 +15,10 @@ let DEV_MODE: string | undefined;
try {
DEV_MODE = Deno.env.get("DENO_POSTGRES_DEVELOPMENT");
} catch (e) {
- if (e instanceof Deno.errors.PermissionDenied) {
+ if (
+ e instanceof Deno.errors.PermissionDenied ||
+ ("NotCapable" in Deno.errors && e instanceof Deno.errors.NotCapable)
+ ) {
throw new Error(
"You need to provide ENV access in order to run the test suite",
);
diff --git a/tests/connection_params_test.ts b/tests/connection_params_test.ts
index d5138784..94df4338 100644
--- a/tests/connection_params_test.ts
+++ b/tests/connection_params_test.ts
@@ -1,4 +1,5 @@
-import { assertEquals, assertThrows, fromFileUrl } from "./test_deps.ts";
+import { assertEquals, assertThrows } from "jsr:@std/assert@1.0.10";
+import { fromFileUrl } from "@std/path";
import { createParams } from "../connection/connection_params.ts";
import { ConnectionParamsError } from "../client/error.ts";
diff --git a/tests/connection_test.ts b/tests/connection_test.ts
index 5cc85539..50cc7dd9 100644
--- a/tests/connection_test.ts
+++ b/tests/connection_test.ts
@@ -1,9 +1,5 @@
-import {
- assertEquals,
- assertRejects,
- copyStream,
- joinPath,
-} from "./test_deps.ts";
+import { assertEquals, assertRejects } from "jsr:@std/assert@1.0.10";
+import { join as joinPath } from "@std/path";
import {
getClearConfiguration,
getClearSocketConfiguration,
@@ -25,26 +21,20 @@ function createProxy(
const proxy = (async () => {
for await (const conn of target) {
- let aborted = false;
-
const outbound = await Deno.connect({
hostname: source.hostname,
port: source.port,
});
+
aborter.signal.addEventListener("abort", () => {
conn.close();
outbound.close();
- aborted = true;
});
+
await Promise.all([
- copyStream(conn, outbound),
- copyStream(outbound, conn),
+ conn.readable.pipeTo(outbound.writable),
+ outbound.readable.pipeTo(conn.writable),
]).catch(() => {});
-
- if (!aborted) {
- conn.close();
- outbound.close();
- }
}
})();
@@ -399,7 +389,7 @@ Deno.test("Closes connection on bad TLS availability verification", async functi
await client.connect();
} catch (e) {
if (
- e instanceof Error ||
+ e instanceof Error &&
e.message.startsWith("Could not check if server accepts SSL connections")
) {
bad_tls_availability_message = true;
@@ -586,19 +576,19 @@ Deno.test("Attempts reconnection on socket disconnection", async () => {
// TODO
// Find a way to unlink the socket to simulate unexpected socket disconnection
-Deno.test("Attempts reconnection when connection is lost", async function () {
+Deno.test("Attempts reconnection when connection is lost", async () => {
const cfg = getMainConfiguration();
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
const { aborter, proxy } = createProxy(listener, {
hostname: cfg.hostname,
- port: Number(cfg.port),
+ port: cfg.port,
});
const client = new Client({
...cfg,
hostname: "127.0.0.1",
- port: (listener.addr as Deno.NetAddr).port,
+ port: listener.addr.port,
tls: {
enabled: false,
},
diff --git a/tests/data_types_test.ts b/tests/data_types_test.ts
index d4d56103..1dc1c463 100644
--- a/tests/data_types_test.ts
+++ b/tests/data_types_test.ts
@@ -1,4 +1,5 @@
-import { assertEquals, base64, formatDate, parseDate } from "./test_deps.ts";
+import { assertEquals } from "jsr:@std/assert@1.0.10";
+import { decodeBase64, encodeBase64 } from "@std/encoding/base64";
import { getMainConfiguration } from "./config.ts";
import { generateSimpleClientTest } from "./helpers.ts";
import type {
@@ -34,7 +35,7 @@ function generateRandomPoint(max_value = 100): Point {
const CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
function randomBase64(): string {
- return base64.encodeBase64(
+ return encodeBase64(
Array.from(
{ length: Math.ceil(Math.random() * 256) },
() => CHARS[Math.floor(Math.random() * CHARS.length)],
@@ -671,7 +672,7 @@ Deno.test(
`SELECT decode('${base64_string}','base64')`,
);
- assertEquals(result.rows[0][0], base64.decodeBase64(base64_string));
+ assertEquals(result.rows[0][0], decodeBase64(base64_string));
}),
);
@@ -691,7 +692,7 @@ Deno.test(
assertEquals(
result.rows[0][0],
- strings.map(base64.decodeBase64),
+ strings.map(decodeBase64),
);
}),
);
@@ -931,7 +932,7 @@ Deno.test(
);
assertEquals(result.rows[0], [
- parseDate(date_text, "yyyy-MM-dd"),
+ new Date(date_text),
Infinity,
]);
}),
@@ -941,7 +942,7 @@ Deno.test(
"date array",
testClient(async (client) => {
await client.queryArray(`SET SESSION TIMEZONE TO '${timezone}'`);
- const dates = ["2020-01-01", formatDate(new Date(), "yyyy-MM-dd")];
+ const dates = ["2020-01-01", (new Date()).toISOString().split("T")[0]];
const { rows: result } = await client.queryArray<[[Date, Date]]>(
"SELECT ARRAY[$1::DATE, $2]",
@@ -950,7 +951,7 @@ Deno.test(
assertEquals(
result[0][0],
- dates.map((d) => parseDate(d, "yyyy-MM-dd")),
+ dates.map((d) => new Date(d)),
);
}),
);
diff --git a/tests/decode_test.ts b/tests/decode_test.ts
index 06512911..b2f0657f 100644
--- a/tests/decode_test.ts
+++ b/tests/decode_test.ts
@@ -17,7 +17,7 @@ import {
decodePoint,
decodeTid,
} from "../query/decoders.ts";
-import { assertEquals, assertThrows } from "./test_deps.ts";
+import { assertEquals, assertThrows } from "jsr:@std/assert@1.0.10";
import { Oid } from "../query/oid.ts";
Deno.test("decodeBigint", function () {
diff --git a/tests/encode_test.ts b/tests/encode_test.ts
index 784fdaab..eab21868 100644
--- a/tests/encode_test.ts
+++ b/tests/encode_test.ts
@@ -1,4 +1,4 @@
-import { assertEquals } from "./test_deps.ts";
+import { assertEquals } from "jsr:@std/assert@1.0.10";
import { encodeArgument } from "../query/encode.ts";
// internally `encodeArguments` uses `getTimezoneOffset` to encode Date
diff --git a/tests/helpers.ts b/tests/helpers.ts
index d1630d3e..e26a7f27 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -1,6 +1,6 @@
import { Client } from "../client.ts";
import { Pool } from "../pool.ts";
-import { type ClientOptions } from "../connection/connection_params.ts";
+import type { ClientOptions } from "../connection/connection_params.ts";
export function generateSimpleClientTest(
client_options: ClientOptions,
diff --git a/tests/pool_test.ts b/tests/pool_test.ts
index fb7c3fcb..3acf920e 100644
--- a/tests/pool_test.ts
+++ b/tests/pool_test.ts
@@ -1,4 +1,4 @@
-import { assertEquals, delay } from "./test_deps.ts";
+import { assertEquals } from "jsr:@std/assert@1.0.10";
import { getMainConfiguration } from "./config.ts";
import { generatePoolClientTest } from "./helpers.ts";
@@ -11,7 +11,7 @@ Deno.test(
assertEquals(POOL.available, 10);
const client = await POOL.connect();
const p = client.queryArray("SELECT pg_sleep(0.1) is null, -1 AS id");
- await delay(1);
+ await new Promise((resolve) => setTimeout(resolve, 1));
assertEquals(POOL.available, 9);
assertEquals(POOL.size, 10);
await p;
@@ -28,7 +28,7 @@ Deno.test(
return query;
});
const qsPromises = Promise.all(qsThunks);
- await delay(1);
+ await new Promise((resolve) => setTimeout(resolve, 1));
assertEquals(POOL.available, 0);
const qs = await qsPromises;
assertEquals(POOL.available, 10);
@@ -52,7 +52,7 @@ Deno.test(
const client_2 = await POOL.connect();
const p = client_2.queryArray("SELECT pg_sleep(0.1) is null, -1 AS id");
- await delay(1);
+ await new Promise((resolve) => setTimeout(resolve, 1));
assertEquals(POOL.size, size);
assertEquals(POOL.available, size - 1);
assertEquals(await POOL.initialized(), 0);
@@ -75,7 +75,7 @@ Deno.test(
},
);
const qsPromises = Promise.all(qsThunks);
- await delay(1);
+ await new Promise((resolve) => setTimeout(resolve, 1));
assertEquals(POOL.available, 0);
assertEquals(await POOL.initialized(), 0);
const qs = await qsPromises;
@@ -140,3 +140,15 @@ Deno.test(
);
}),
);
+
+Deno.test(
+ "Pool client will be released after `using` block",
+ testPool(async (POOL) => {
+ const initialPoolAvailable = POOL.available;
+ {
+ using _client = await POOL.connect();
+ assertEquals(POOL.available, initialPoolAvailable - 1);
+ }
+ assertEquals(POOL.available, initialPoolAvailable);
+ }),
+);
diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts
index 0e71da69..26966de4 100644
--- a/tests/query_client_test.ts
+++ b/tests/query_client_test.ts
@@ -12,10 +12,10 @@ import {
assertObjectMatch,
assertRejects,
assertThrows,
-} from "./test_deps.ts";
+} from "jsr:@std/assert@1.0.10";
import { getMainConfiguration } from "./config.ts";
-import { PoolClient, QueryClient } from "../client.ts";
-import { ClientOptions } from "../connection/connection_params.ts";
+import type { PoolClient, QueryClient } from "../client.ts";
+import type { ClientOptions } from "../connection/connection_params.ts";
import { Oid } from "../query/oid.ts";
function withClient(
@@ -241,6 +241,79 @@ Deno.test(
),
);
+Deno.test(
+ "Custom decoders with arrays",
+ withClient(
+ async (client) => {
+ const result = await client.queryObject(
+ `SELECT
+ ARRAY[true, false, true] AS _bool_array,
+ ARRAY['2024-01-01'::date, '2024-01-02'::date, '2024-01-03'::date] AS _date_array,
+ ARRAY[1.5:: REAL, 2.5::REAL, 3.5::REAL] AS _float_array,
+ ARRAY[10, 20, 30] AS _int_array,
+ ARRAY[
+ '{"key1": "value1", "key2": "value2"}'::jsonb,
+ '{"key3": "value3", "key4": "value4"}'::jsonb,
+ '{"key5": "value5", "key6": "value6"}'::jsonb
+ ] AS _jsonb_array,
+ ARRAY['string1', 'string2', 'string3'] AS _text_array
+ ;`,
+ );
+
+ assertEquals(result.rows, [
+ {
+ _bool_array: [
+ { boolean: true },
+ { boolean: false },
+ { boolean: true },
+ ],
+ _date_array: [
+ new Date("2024-01-11T00:00:00.000Z"),
+ new Date("2024-01-12T00:00:00.000Z"),
+ new Date("2024-01-13T00:00:00.000Z"),
+ ],
+ _float_array: [15, 25, 35],
+ _int_array: [110, 120, 130],
+ _jsonb_array: [
+ { key1: "value1", key2: "value2" },
+ { key3: "value3", key4: "value4" },
+ { key5: "value5", key6: "value6" },
+ ],
+ _text_array: ["string1_!", "string2_!", "string3_!"],
+ },
+ ]);
+ },
+ {
+ controls: {
+ decoders: {
+ // convert to object
+ [Oid.bool]: (value: string) => ({ boolean: value === "t" }),
+ // 1082 = date : convert to date and add 10 days
+ "1082": (value: string) => {
+ const d = new Date(value);
+ return new Date(d.setDate(d.getDate() + 10));
+ },
+ // multiply by 20, should not be used!
+ float4: (value: string) => parseFloat(value) * 20,
+ // multiply by 10
+ float4_array: (value: string, _, parseArray) =>
+ parseArray(value, (v) => parseFloat(v) * 10),
+ // return 0, should not be used!
+ [Oid.int4]: () => 0,
+ // add 100
+ [Oid.int4_array]: (value: string, _, parseArray) =>
+ parseArray(value, (v) => parseInt(v, 10) + 100),
+ // split string and reverse, should not be used!
+ [Oid.text]: (value: string) => value.split("").reverse(),
+ // 1009 = text_array : append "_!" to each string
+ 1009: (value: string, _, parseArray) =>
+ parseArray(value, (v) => `${v}_!`),
+ },
+ },
+ },
+ ),
+);
+
Deno.test(
"Custom decoder precedence",
withClient(
diff --git a/tests/test_deps.ts b/tests/test_deps.ts
index 3ec05aaa..cb56ee54 100644
--- a/tests/test_deps.ts
+++ b/tests/test_deps.ts
@@ -1,4 +1,3 @@
-export * from "../deps.ts";
export {
assert,
assertEquals,
@@ -7,6 +6,4 @@ export {
assertObjectMatch,
assertRejects,
assertThrows,
-} from "https://deno.land/std@0.214.0/assert/mod.ts";
-export { format as formatDate } from "https://deno.land/std@0.214.0/datetime/format.ts";
-export { copy as copyStream } from "https://deno.land/std@0.214.0/io/copy.ts";
+} from "jsr:@std/assert@1.0.10";
diff --git a/tests/utils_test.ts b/tests/utils_test.ts
index d5e418d3..40542ea7 100644
--- a/tests/utils_test.ts
+++ b/tests/utils_test.ts
@@ -1,5 +1,5 @@
-import { assertEquals, assertThrows } from "./test_deps.ts";
-import { parseConnectionUri, Uri } from "../utils/utils.ts";
+import { assertEquals, assertThrows } from "jsr:@std/assert@1.0.10";
+import { parseConnectionUri, type Uri } from "../utils/utils.ts";
import { DeferredAccessStack, DeferredStack } from "../utils/deferred.ts";
class LazilyInitializedObject {
diff --git a/tools/convert_to_jsr.ts b/tools/convert_to_jsr.ts
deleted file mode 100644
index 9843f572..00000000
--- a/tools/convert_to_jsr.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { walk } from "https://deno.land/std@0.214.0/fs/walk.ts";
-import denoConfig from "../deno.json" with { type: "json" };
-
-const STD_SPECIFIER_REGEX =
- /https:\/\/deno\.land\/std@(\d+\.\d+\.\d+)\/(\w+)\/(.+)\.ts/g;
-const POSTGRES_X_SPECIFIER = "https://deno.land/x/postgres/mod.ts";
-const POSTGRES_JSR_SPECIFIER = `jsr:${denoConfig.name}`;
-
-function toStdJsrSpecifier(
- _full: string,
- _version: string,
- module: string,
- path: string,
-): string {
- /**
- * @todo(iuioiua) Restore the dynamic use of the `version` argument
- * once 0.214.0 is released.
- */
- const version = "0.213.1";
- return path === "mod"
- ? `jsr:@std/${module}@${version}`
- : `jsr:@std/${module}@${version}/${path}`;
-}
-
-for await (
- const entry of walk(".", {
- includeDirs: false,
- exts: [".ts", ".md"],
- skip: [/docker/, /.github/, /tools/],
- followSymlinks: false,
- })
-) {
- const text = await Deno.readTextFile(entry.path);
- const newText = text
- .replaceAll(STD_SPECIFIER_REGEX, toStdJsrSpecifier)
- .replaceAll(POSTGRES_X_SPECIFIER, POSTGRES_JSR_SPECIFIER);
- await Deno.writeTextFile(entry.path, newText);
-}
diff --git a/utils/deferred.ts b/utils/deferred.ts
index f4b4c10a..9d650d90 100644
--- a/utils/deferred.ts
+++ b/utils/deferred.ts
@@ -22,7 +22,9 @@ export class DeferredStack {
async pop(): Promise {
if (this.#elements.length > 0) {
return this.#elements.pop()!;
- } else if (this.#size < this.#max_size && this.#creator) {
+ }
+
+ if (this.#size < this.#max_size && this.#creator) {
this.#size++;
return await this.#creator();
}
diff --git a/utils/utils.ts b/utils/utils.ts
index ae7ccee8..f0280fb7 100644
--- a/utils/utils.ts
+++ b/utils/utils.ts
@@ -1,4 +1,4 @@
-import { bold, yellow } from "../deps.ts";
+import { bold, yellow } from "@std/fmt/colors";
export function readInt16BE(buffer: Uint8Array, offset: number): number {
offset = offset >>> 0;
@@ -93,7 +93,7 @@ export function parseConnectionUri(uri: string): Uri {
}
} catch (_e) {
console.error(
- bold(yellow("Failed to decode URL host") + "\nDefaulting to raw host"),
+ bold(`${yellow("Failed to decode URL host")}\nDefaulting to raw host`),
);
}
@@ -108,8 +108,9 @@ export function parseConnectionUri(uri: string): Uri {
} catch (_e) {
console.error(
bold(
- yellow("Failed to decode URL password") +
- "\nDefaulting to raw password",
+ `${
+ yellow("Failed to decode URL password")
+ }\nDefaulting to raw password`,
),
);
}