+
+
+[](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).
-## Example
+
+
+## Documentation
+
+The documentation is available on the
+[`deno-postgres`](https://deno-postgres.com/) website.
+
+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",
@@ -24,6 +42,7 @@ const client = new Client({
hostname: "localhost",
port: 5432,
});
+
await client.connect();
{
@@ -51,16 +70,41 @@ await client.connect();
await client.end();
```
-For more examples, visit the documentation available at
-[https://deno-postgres.com/](https://deno-postgres.com/)
+## Deno compatibility
-## Documentation
+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
-The documentation is available on the deno-postgres website
-[https://deno-postgres.com/](https://deno-postgres.com/)
+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.
-Join the [Discord](https://discord.gg/HEdTCvZUSf) as well! It's a good place to
-discuss bugs and features before opening issues.
+## 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
@@ -76,8 +120,8 @@ discuss bugs and features before opening issues.
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
@@ -89,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
@@ -99,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 () => {
@@ -137,25 +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 |
-
## Contributing guidelines
When contributing to the repository, make sure to:
@@ -181,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 a7b97566..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";
}
@@ -35,12 +35,18 @@ export class PostgresError extends Error {
*/
public fields: Notice;
+ /**
+ * The query that caused the error
+ */
+ public query: string | undefined;
+
/**
* Create a new PostgresError
*/
- constructor(fields: Notice) {
+ constructor(fields: Notice, query?: string) {
super(fields.message);
this.fields = fields;
+ this.query = query;
this.name = "PostgresError";
}
}
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 c062553c..9c0e66a2 100644
--- a/connection/connection.ts
+++ b/connection/connection.ts
@@ -26,14 +26,8 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import {
- bold,
- BufReader,
- BufWriter,
- delay,
- joinPath,
- 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";
@@ -53,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,
@@ -68,6 +62,7 @@ import {
INCOMING_TLS_MESSAGES,
} from "./message_code.ts";
import { hashMd5Password } from "./auth.ts";
+import { isDebugOptionEnabled } from "../debug.ts";
// Work around unstable limitation
type ConnectOptions =
@@ -97,7 +92,25 @@ function assertSuccessfulAuthentication(auth_message: Message) {
}
function logNotice(notice: Notice) {
- console.error(`${bold(yellow(notice.severity))}: ${notice.message}`);
+ if (notice.severity === "INFO") {
+ console.info(
+ `[ ${bold(rgb24(notice.severity, 0xff99ff))} ] : ${notice.message}`,
+ );
+ } else if (notice.severity === "NOTICE") {
+ console.info(`[ ${bold(yellow(notice.severity))} ] : ${notice.message}`);
+ } else if (notice.severity === "WARNING") {
+ console.warn(
+ `[ ${bold(rgb24(notice.severity, 0xff9900))} ] : ${notice.message}`,
+ );
+ }
+}
+
+function logQuery(query: string) {
+ console.info(`[ ${bold(rgb24("QUERY", 0x00ccff))} ] : ${query}`);
+}
+
+function logResults(rows: unknown[]) {
+ console.info(`[ ${bold(rgb24("RESULTS", 0x00cc00))} ] :`, rows);
}
const decoder = new TextDecoder();
@@ -107,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;
@@ -122,6 +133,7 @@ export class Connection {
#secretKey?: number;
#tls?: boolean;
#transport?: "tcp" | "socket";
+ #connWritable!: WritableStreamDefaultWriter;
get pid(): number | undefined {
return this.#pid;
@@ -145,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
@@ -167,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);
}
@@ -177,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);
@@ -234,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();
}
@@ -244,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) {
@@ -275,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() {
@@ -318,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";
@@ -334,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,
});
@@ -343,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"),
);
@@ -370,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(
@@ -411,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}`);
}
@@ -443,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) {
@@ -467,7 +509,7 @@ export class Connection {
}
if (interval > 0) {
- await delay(interval);
+ await new Promise((resolve) => setTimeout(resolve, interval));
}
}
try {
@@ -541,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();
}
@@ -563,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();
}
@@ -591,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) {
@@ -619,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) {
@@ -656,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) {
@@ -666,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
@@ -674,7 +711,15 @@ export class Connection {
while (current_message.type !== INCOMING_QUERY_MESSAGES.READY) {
switch (current_message.type) {
case ERROR_MESSAGE:
- error = new PostgresError(parseNoticeMessage(current_message));
+ error = new PostgresError(
+ parseNoticeMessage(current_message),
+ isDebugOptionEnabled(
+ "queryInError",
+ this.#connection_params.controls?.debug,
+ )
+ ? query.text
+ : undefined,
+ );
break;
case INCOMING_QUERY_MESSAGES.COMMAND_COMPLETE: {
result.handleCommandComplete(
@@ -695,7 +740,14 @@ export class Connection {
break;
case INCOMING_QUERY_MESSAGES.NOTICE_WARNING: {
const notice = parseNoticeMessage(current_message);
- logNotice(notice);
+ if (
+ isDebugOptionEnabled(
+ "notices",
+ this.#connection_params.controls?.debug,
+ )
+ ) {
+ logNotice(notice);
+ }
result.warnings.push(notice);
break;
}
@@ -731,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) {
@@ -748,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) {
@@ -768,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);
}
/**
@@ -783,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() {
@@ -793,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
@@ -819,6 +871,12 @@ export class Connection {
/**
* https://www.postgresql.org/docs/14/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY
*/
+ async #preparedQuery(
+ query: Query,
+ ): Promise;
+ async #preparedQuery(
+ query: Query,
+ ): Promise;
async #preparedQuery(
query: Query,
): Promise {
@@ -832,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) {
@@ -842,13 +898,21 @@ 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) {
switch (current_message.type) {
case ERROR_MESSAGE: {
- error = new PostgresError(parseNoticeMessage(current_message));
+ error = new PostgresError(
+ parseNoticeMessage(current_message),
+ isDebugOptionEnabled(
+ "queryInError",
+ this.#connection_params.controls?.debug,
+ )
+ ? query.text
+ : undefined,
+ );
break;
}
case INCOMING_QUERY_MESSAGES.BIND_COMPLETE:
@@ -872,7 +936,14 @@ export class Connection {
break;
case INCOMING_QUERY_MESSAGES.NOTICE_WARNING: {
const notice = parseNoticeMessage(current_message);
- logNotice(notice);
+ if (
+ isDebugOptionEnabled(
+ "notices",
+ this.#connection_params.controls?.debug,
+ )
+ ) {
+ logNotice(notice);
+ }
result.warnings.push(notice);
break;
}
@@ -911,11 +982,23 @@ export class Connection {
await this.#queryLock.pop();
try {
+ if (
+ isDebugOptionEnabled("queries", this.#connection_params.controls?.debug)
+ ) {
+ logQuery(query.text);
+ }
+ let result: QueryArrayResult | QueryObjectResult;
if (query.args.length === 0) {
- return await this.#simpleQuery(query);
+ result = await this.#simpleQuery(query);
} else {
- return await this.#preparedQuery(query);
+ result = await this.#preparedQuery(query);
+ }
+ if (
+ isDebugOptionEnabled("results", this.#connection_params.controls?.debug)
+ ) {
+ logResults(result.rows);
}
+ return result;
} catch (e) {
if (e instanceof ConnectionError) {
await this.end();
@@ -929,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 ec4d07eb..a55fb804 100644
--- a/connection/connection_params.ts
+++ b/connection/connection_params.ts
@@ -1,7 +1,9 @@
import { parseConnectionUri } from "../utils/utils.ts";
import { ConnectionParamsError } from "../client/error.ts";
-import { fromFileUrl, isAbsolute } from "../deps.ts";
-import { OidKey } from "../query/oid.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
@@ -92,21 +94,40 @@ export interface TLSOptions {
caCertificates: string[];
}
+/**
+ * The strategy to use when decoding results data
+ */
export type DecodeStrategy = "string" | "auto";
+/**
+ * A dictionary of functions used to decode (parse) column field values from string to a custom type. These functions will
+ * take precedence over the {@linkcode DecodeStrategy}. Each key in the dictionary is the column OID type number or Oid type name,
+ * and the value is the decoder function.
+ */
export type Decoders = {
- [key in number | OidKey]?: DecoderFunction;
+ [key in number | OidType]?: DecoderFunction;
};
/**
* 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
*/
export type ClientControls = {
+ /**
+ * Debugging options
+ */
+ debug?: DebugControls;
/**
* The strategy to use when decoding results data
*
@@ -128,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)),
* }
@@ -399,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/debug.ts b/debug.ts
new file mode 100644
index 00000000..1b477888
--- /dev/null
+++ b/debug.ts
@@ -0,0 +1,30 @@
+/**
+ * Controls debugging behavior. If set to `true`, all debug options are enabled.
+ * If set to `false`, all debug options are disabled. Can also be an object with
+ * specific debug options to enable.
+ *
+ * {@default false}
+ */
+export type DebugControls = DebugOptions | boolean;
+
+type DebugOptions = {
+ /** Log all queries */
+ queries?: boolean;
+ /** Log all INFO, NOTICE, and WARNING raised database messages */
+ notices?: boolean;
+ /** Log all results */
+ results?: boolean;
+ /** Include the SQL query that caused an error in the PostgresError object */
+ queryInError?: boolean;
+};
+
+export const isDebugOptionEnabled = (
+ option: keyof DebugOptions,
+ options?: DebugControls,
+): boolean => {
+ if (typeof options === "boolean") {
+ return options;
+ }
+
+ return !!options?.[option];
+};
diff --git a/deno.json b/deno.json
index a25df52d..35e10847 100644
--- a/deno.json
+++ b/deno.json
@@ -1,6 +1,14 @@
{
- "lock": false,
- "name": "@bartlomieju/postgres",
- "version": "0.18.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 1dcd6cea..00000000
--- a/deps.ts
+++ /dev/null
@@ -1,14 +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, 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 528c2d25..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 }
}
```
@@ -856,14 +908,14 @@ for that such as aliasing every query field that is done to the database, one
easy built-in solution allows developers to transform the incoming query names
into the casing of their preference without any extra steps
-##### Camelcase
+##### Camel case
-To transform a query result into camelcase, you only need to provide the
-`camelcase` option on your query call
+To transform a query result into camel case, you only need to provide the
+`camelCase` option on your query call
```ts
const { rows: result } = await client.queryObject({
- camelcase: true,
+ camelCase: true,
text: "SELECT FIELD_X, FIELD_Y FROM MY_TABLE",
});
@@ -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;
}
}
@@ -1393,3 +1445,61 @@ await transaction.queryArray`INSERT INTO DONT_DELETE_ME VALUES (2)`; // Still in
await transaction.commit();
// Transaction ends, client gets unlocked
```
+
+## Debugging
+
+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 granularity by enabling the following
+options:
+
+- `queries` : Logs all SQL queries executed by the client
+- `notices` : Logs all database messages (INFO, NOTICE, WARNING))
+- `results` : Logs all the result of the queries
+- `queryInError` : Includes the SQL query that caused an error in the
+ PostgresError object
+
+### Example
+
+```ts
+// debug_test.ts
+import { Client } from "jsr:@db/postgres";
+
+const client = new Client({
+ user: "postgres",
+ database: "postgres",
+ hostname: "localhost",
+ port: 5432,
+ password: "postgres",
+ controls: {
+ debug: {
+ queries: true,
+ notices: true,
+ results: true,
+ },
+ },
+});
+
+await client.connect();
+
+await client.queryObject`SELECT public.get_uuid()`;
+
+await client.end();
+```
+
+```sql
+-- example database function that raises messages
+CREATE OR REPLACE FUNCTION public.get_uuid()
+ RETURNS uuid LANGUAGE plpgsql
+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 length in order to be valid!';
+ RETURN gen_random_uuid();
+ END;
+$function$;;
+```
+
+
diff --git a/docs/debug-output.png b/docs/debug-output.png
new file mode 100644
index 00000000..02277a8d
Binary files /dev/null and b/docs/debug-output.png differ
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 143abffc..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 { OidKey, OidType } 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 2904567d..c0311910 100644
--- a/query/decode.ts
+++ b/query/decode.ts
@@ -1,5 +1,5 @@
-import { Oid, OidType, OidTypes } 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 OidType]];
+ 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/oid.ts b/query/oid.ts
index 9b36c88b..93c03ec2 100644
--- a/query/oid.ts
+++ b/query/oid.ts
@@ -1,8 +1,10 @@
-export type OidKey = keyof typeof Oid;
-export type OidType = (typeof Oid)[OidKey];
+/** A Postgres Object identifiers (OIDs) type name. */
+export type OidType = keyof typeof Oid;
+/** A Postgres Object identifiers (OIDs) numeric value. */
+export type OidValue = (typeof Oid)[OidType];
/**
- * Oid is a map of OidKey to OidType.
+ * A map of OidType to OidValue.
*/
export const Oid = {
bool: 16,
@@ -175,11 +177,10 @@ export const Oid = {
} as const;
/**
- * OidTypes is a map of OidType to OidKey.
- * Used to decode values and avoid search iteration
+ * A map of OidValue to OidType. Used to decode values and avoid search iteration.
*/
export const OidTypes: {
- [key in OidType]: OidKey;
+ [key in OidValue]: OidType;
} = {
16: "bool",
17: "bytea",
diff --git a/query/query.ts b/query/query.ts
index 0bb39d7b..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 {
@@ -132,19 +134,19 @@ export interface QueryObjectOptions extends QueryOptions {
// TODO
// Support multiple case options
/**
- * Enabling camelcase will transform any snake case field names coming from the database into camel case ones
+ * Enabling camel case will transform any snake case field names coming from the database into camel case ones
*
* Ex: `SELECT 1 AS my_field` will return `{ myField: 1 }`
*
* This won't have any effect if you explicitly set the field names with the `fields` parameter
*/
- camelcase?: boolean;
+ camelCase?: boolean;
/**
* This parameter supersedes query column names coming from the databases in the order they were provided.
* Fields must be unique and be in the range of (a-zA-Z0-9_), otherwise the query will throw before execution.
* A field can not start with a number, just like JavaScript variables
*
- * This setting overrides the camelcase option
+ * This setting overrides the camel case option
*
* Ex: `SELECT 'A', 'B' AS my_field` with fields `["field_1", "field_2"]` will return `{ field_1: "A", field_2: "B" }`
*/
@@ -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;
}
/**
@@ -324,7 +324,7 @@ export class QueryObjectResult<
this.columns = this.query.fields;
} else {
let column_names: string[];
- if (this.query.camelcase) {
+ if (this.query.camelCase) {
column_names = this.rowDescription.columns.map((column) =>
snakecaseToCamelcase(column.name)
);
@@ -380,7 +380,7 @@ export class QueryObjectResult<
*/
export class Query {
public args: EncodedArg[];
- public camelcase?: boolean;
+ public camelCase?: boolean;
/**
* The explicitly set fields for the query result, they have been validated beforehand
* for duplicates and invalid names
@@ -408,7 +408,7 @@ export class Query {
this.text = config_or_text;
this.args = args.map(encodeArgument);
} else {
- const { camelcase, encoder = encodeArgument, fields } = config_or_text;
+ const { camelCase, encoder = encodeArgument, fields } = config_or_text;
let { args = [], text } = config_or_text;
// Check that the fields passed are valid and can be used to map
@@ -432,7 +432,7 @@ export class Query {
this.fields = fields;
}
- this.camelcase = camelcase;
+ this.camelCase = camelCase;
if (!Array.isArray(args)) {
[text, args] = objectQueryToQueryArgs(text, args);
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 84e05f94..26966de4 100644
--- a/tests/query_client_test.ts
+++ b/tests/query_client_test.ts
@@ -8,13 +8,14 @@ import {
import {
assert,
assertEquals,
+ assertInstanceOf,
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(
@@ -240,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(
@@ -284,6 +358,43 @@ Deno.test(
),
);
+Deno.test(
+ "Debug query not in error",
+ withClient(async (client) => {
+ const invalid_query = "SELECT this_has $ 'syntax_error';";
+ try {
+ await client.queryObject(invalid_query);
+ } catch (error) {
+ assertInstanceOf(error, PostgresError);
+ assertEquals(error.message, 'syntax error at or near "$"');
+ assertEquals(error.query, undefined);
+ }
+ }),
+);
+
+Deno.test(
+ "Debug query in error",
+ withClient(
+ async (client) => {
+ const invalid_query = "SELECT this_has $ 'syntax_error';";
+ try {
+ await client.queryObject(invalid_query);
+ } catch (error) {
+ assertInstanceOf(error, PostgresError);
+ assertEquals(error.message, 'syntax error at or near "$"');
+ assertEquals(error.query, invalid_query);
+ }
+ },
+ {
+ controls: {
+ debug: {
+ queryInError: true,
+ },
+ },
+ },
+ ),
+);
+
Deno.test(
"Array arguments",
withClient(async (client) => {
@@ -796,7 +907,7 @@ Deno.test(
);
Deno.test(
- "Object query field names aren't transformed when camelcase is disabled",
+ "Object query field names aren't transformed when camel case is disabled",
withClient(async (client) => {
const record = {
pos_x: "100",
@@ -806,7 +917,7 @@ Deno.test(
const { rows: result } = await client.queryObject({
args: [record.pos_x, record.pos_y, record.prefix_name_suffix],
- camelcase: false,
+ camelCase: false,
text: "SELECT $1 AS POS_X, $2 AS POS_Y, $3 AS PREFIX_NAME_SUFFIX",
});
@@ -815,7 +926,7 @@ Deno.test(
);
Deno.test(
- "Object query field names are transformed when camelcase is enabled",
+ "Object query field names are transformed when camel case is enabled",
withClient(async (client) => {
const record = {
posX: "100",
@@ -825,7 +936,7 @@ Deno.test(
const { rows: result } = await client.queryObject({
args: [record.posX, record.posY, record.prefixNameSuffix],
- camelcase: true,
+ camelCase: true,
text: "SELECT $1 AS POS_X, $2 AS POS_Y, $3 AS PREFIX_NAME_SUFFIX",
});
@@ -846,13 +957,13 @@ Deno.test(
);
Deno.test(
- "Object query explicit fields override camelcase",
+ "Object query explicit fields override camel case",
withClient(async (client) => {
const record = { field_1: "A", field_2: "B", field_3: "C" };
const { rows: result } = await client.queryObject({
args: [record.field_1, record.field_2, record.field_3],
- camelcase: true,
+ camelCase: true,
fields: ["field_1", "field_2", "field_3"],
text: "SELECT $1 AS POS_X, $2 AS POS_Y, $3 AS PREFIX_NAME_SUFFIX",
});
@@ -888,7 +999,7 @@ Deno.test(
await assertRejects(
() =>
client.queryObject({
- camelcase: true,
+ camelCase: true,
text: `SELECT 1 AS "fieldX", 2 AS field_x`,
}),
Error,
diff --git a/tests/test_deps.ts b/tests/test_deps.ts
index 1fce7027..cb56ee54 100644
--- a/tests/test_deps.ts
+++ b/tests/test_deps.ts
@@ -1,11 +1,9 @@
-export * from "../deps.ts";
export {
assert,
assertEquals,
+ assertInstanceOf,
assertNotEquals,
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`,
),
);
}