From 52322dadfa5e6db010a8c931a8b2394517a24923 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Tue, 6 Feb 2024 15:33:38 +1100 Subject: [PATCH 01/13] docs: remove pinned version imports (#452) --- README.md | 4 ++-- docs/README.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0c1aa31a..44394f46 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Build Status](https://img.shields.io/github/workflow/status/denodrivers/postgres/ci?label=Build&logo=github&style=flat-square) [![Discord server](https://img.shields.io/discord/768918486575480863?color=blue&label=Ask%20for%20help%20here&logo=discord&style=flat-square)](https://discord.gg/HEdTCvZUSf) [![Manual](https://img.shields.io/github/v/release/denodrivers/postgres?color=orange&label=Manual&logo=deno&style=flat-square)](https://deno-postgres.com) -[![Documentation](https://img.shields.io/github/v/release/denodrivers/postgres?color=yellow&label=Documentation&logo=deno&style=flat-square)](https://doc.deno.land/https/deno.land/x/postgres@v0.17.1/mod.ts) +[![Documentation](https://img.shields.io/github/v/release/denodrivers/postgres?color=yellow&label=Documentation&logo=deno&style=flat-square)](https://doc.deno.land/https/deno.land/x/postgres/mod.ts) [![License](https://img.shields.io/github/license/denodrivers/postgres?color=yellowgreen&label=License&style=flat-square)](LICENSE) A lightweight PostgreSQL driver for Deno focused on user experience @@ -16,7 +16,7 @@ A lightweight PostgreSQL driver for Deno focused on user experience ```ts // deno run --allow-net --allow-read mod.ts -import { Client } from "https://deno.land/x/postgres@v0.17.1/mod.ts"; +import { Client } from "https://deno.land/x/postgres/mod.ts"; const client = new Client({ user: "user", diff --git a/docs/README.md b/docs/README.md index 2135eb05..11eb512e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ ![Build Status](https://img.shields.io/github/workflow/status/denodrivers/postgres/ci?label=Build&logo=github&style=flat-square) [![Discord server](https://img.shields.io/discord/768918486575480863?color=blue&label=Ask%20for%20help%20here&logo=discord&style=flat-square)](https://discord.gg/HEdTCvZUSf) ![Manual](https://img.shields.io/github/v/release/denodrivers/postgres?color=orange&label=Manual&logo=deno&style=flat-square) -[![Documentation](https://img.shields.io/github/v/release/denodrivers/postgres?color=yellow&label=Documentation&logo=deno&style=flat-square)](https://doc.deno.land/https/deno.land/x/postgres@v0.17.1/mod.ts) +[![Documentation](https://img.shields.io/github/v/release/denodrivers/postgres?color=yellow&label=Documentation&logo=deno&style=flat-square)](https://doc.deno.land/https/deno.land/x/postgres/mod.ts) ![License](https://img.shields.io/github/license/denodrivers/postgres?color=yellowgreen&label=License&style=flat-square) `deno-postgres` is a lightweight PostgreSQL driver for Deno focused on user @@ -11,7 +11,7 @@ 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@v0.17.1/mod.ts"; +import { Client } from "https://deno.land/x/postgres/mod.ts"; const client = new Client({ user: "user", @@ -38,7 +38,7 @@ All `deno-postgres` clients provide the following options to authenticate and manage your connections ```ts -import { Client } from "https://deno.land/x/postgres@v0.17.1/mod.ts"; +import { Client } from "https://deno.land/x/postgres/mod.ts"; let config; From b78c2a6c3305594cefc9d6249434d36fdc38037f Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Wed, 7 Feb 2024 10:59:59 +1100 Subject: [PATCH 02/13] feat: JSR + `/x` support (#453) * feat: JSR support * work * work * work * tweak * fmt * tweak * tweak * tweak * tweak * workaround --- .github/workflows/publish_jsr.yml | 53 +++++++++++++++++++++++++++++++ deno.json | 6 ++++ tools/convert_to_jsr.ts | 38 ++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 .github/workflows/publish_jsr.yml create mode 100644 deno.json create mode 100644 tools/convert_to_jsr.ts diff --git a/.github/workflows/publish_jsr.yml b/.github/workflows/publish_jsr.yml new file mode 100644 index 00000000..209bfd04 --- /dev/null +++ b/.github/workflows/publish_jsr.yml @@ -0,0 +1,53 @@ +name: Publish to JSR + +on: + push: + branches: + - main + +env: + DENO_UNSTABLE_WORKSPACES: true + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 30 + + permissions: + contents: read + id-token: write + + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Deno + uses: denoland/setup-deno@v1 + + - name: Convert to JSR package + run: deno run -A tools/convert_to_jsr.ts + + - name: Format + run: deno fmt --check + + - name: Lint + run: deno lint + + - name: Documentation tests + run: deno test --doc client.ts mod.ts pool.ts client/ connection/ query/ utils/ + + - name: Build tests container + run: docker-compose build tests + + - name: Run tests + run: docker-compose run tests + + - name: Publish (dry run) + if: startsWith(github.ref, 'refs/tags/') == false + run: deno publish --dry-run + + - name: Publish (real) + if: startsWith(github.ref, 'refs/tags/') + run: deno publish \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..1fc619c0 --- /dev/null +++ b/deno.json @@ -0,0 +1,6 @@ +{ + "lock": false, + "name": "@bartlomieju/postgres", + "version": "0.17.2", + "exports": "./mod.ts" +} diff --git a/tools/convert_to_jsr.ts b/tools/convert_to_jsr.ts new file mode 100644 index 00000000..9843f572 --- /dev/null +++ b/tools/convert_to_jsr.ts @@ -0,0 +1,38 @@ +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); +} From d0bd5ca121bde8142b1d44efa868668cab7a51ac Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Wed, 7 Feb 2024 12:26:53 +1100 Subject: [PATCH 03/13] chore: cleanups and documentation relating to JSR (#454) * chore: cleanups and documentation relating to JSR * tweak * tweak * fmt --- .github/workflows/publish_jsr.yml | 3 --- README.md | 27 +++++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish_jsr.yml b/.github/workflows/publish_jsr.yml index 209bfd04..b797ff91 100644 --- a/.github/workflows/publish_jsr.yml +++ b/.github/workflows/publish_jsr.yml @@ -5,9 +5,6 @@ on: branches: - main -env: - DENO_UNSTABLE_WORKSPACES: true - jobs: publish: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 44394f46..d41ddf12 100644 --- a/README.md +++ b/README.md @@ -147,16 +147,18 @@ alongside the library This situation will become more stable as `std` and `deno-postgres` approach 1.0 -| Deno version | Min driver version | Max driver version | -| ------------- | ------------------ | ------------------ | -| 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.15.0 | 0.13.0 | | -| 1.16.0 | 0.14.0 | 0.14.3 | -| 1.17.0 | 0.15.0 | | +| Deno version | Min driver version | Max driver version | +| ----------------------------------------------------- | ------------------ | ------------------ | +| 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.15.0 | 0.13.0 | | +| 1.16.0 | 0.14.0 | 0.14.3 | +| 1.17.0 | 0.15.0 | | +| 1.40.0 | 0.17.2 | | +| This module is available on JSR starting from 0.17.2. | | | ## Contributing guidelines @@ -172,6 +174,11 @@ When contributing to repository make sure to: 4. All features and fixes must have a corresponding test added in order to be accepted +## Maintainers guidelines + +When publishing a new version, ensure that the `version` field in `deno.json` +has been updated to match the new version. + ## License There are substantial parts of this library based on other libraries. They have From c7aac9192ba003442a76369046b9650ced1210cd Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Wed, 7 Feb 2024 09:41:24 -0400 Subject: [PATCH 04/13] Update README.md --- README.md | 98 ++++++++++++++++++++++++++----------------------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index d41ddf12..aaa40e4f 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![Documentation](https://img.shields.io/github/v/release/denodrivers/postgres?color=yellow&label=Documentation&logo=deno&style=flat-square)](https://doc.deno.land/https/deno.land/x/postgres/mod.ts) [![License](https://img.shields.io/github/license/denodrivers/postgres?color=yellowgreen&label=License&style=flat-square)](LICENSE) -A lightweight PostgreSQL driver for Deno focused on user experience +A lightweight PostgreSQL driver for Deno focused on user experience. -`deno-postgres` is being developed based on excellent work of +`deno-postgres` is being developed inspired by the excellent work of [node-postgres](https://github.com/brianc/node-postgres) and [pq](https://github.com/lib/pq). @@ -32,8 +32,8 @@ await client.connect(); } { - const result = await client - .queryArray`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; + const result = + await client.queryArray`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; console.log(result.rows); // [[1, 'Carlos']] } @@ -43,15 +43,15 @@ await client.connect(); } { - const result = await client - .queryObject`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; + const result = + await client.queryObject`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; console.log(result.rows); // [{id: 1, name: 'Carlos'}] } await client.end(); ``` -For more examples visit the documentation available at +For more examples, visit the documentation available at [https://deno-postgres.com/](https://deno-postgres.com/) ## Documentation @@ -59,34 +59,36 @@ For more examples visit the documentation available at The documentation is available on the deno-postgres website [https://deno-postgres.com/](https://deno-postgres.com/) -Join me on [Discord](https://discord.gg/HEdTCvZUSf) as well! It's a good place -to discuss bugs and features before opening issues +Join the [Discord](https://discord.gg/HEdTCvZUSf) as well! It's a good place +to discuss bugs and features before opening issues. ## Contributing ### Prerequisites -- You must have `docker` and `docker-compose` installed in your machine +- You must have `docker` and `docker-compose` installed on your machine + - https://docs.docker.com/get-docker/ - https://docs.docker.com/compose/install/ -- You don't need `deno` installed in your machine to run the tests, since it - will be installed in the Docker container when you build it. However you will - need it in order to run the linter and formatter locally +- You don't need `deno` installed in your machine to run the tests since it + will be installed in the Docker container when you build it. However, you will + need it to run the linter and formatter locally + - https://deno.land/ - `deno upgrade --version 1.7.1` - `dvm install 1.7.1 && dvm use 1.7.1` -- You don't need to install Postgres locally in your machine in order to test +- 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 ### Running the tests The tests are found under the `./tests` folder, and they are based on query -result assertions +result assertions. -In order to run the tests run the following commands +To run the tests, run the following commands: 1. `docker-compose build tests` 2. `docker-compose run tests` @@ -95,7 +97,7 @@ The build step will check linting and formatting as well and report it to the command line It is recommended that you don't rely on any previously initialized data for -your tests, instead of that create all the data you need at the moment of +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 @@ -103,12 +105,8 @@ once the test has been completed ```ts Deno.test("INSERT works correctly", async () => { - await client.queryArray( - `CREATE TEMP TABLE MY_TEST (X INTEGER);`, - ); - await client.queryArray( - `INSERT INTO MY_TEST (X) VALUES (1);`, - ); + await client.queryArray(`CREATE TEMP TABLE MY_TEST (X INTEGER);`); + await client.queryArray(`INSERT INTO MY_TEST (X) VALUES (1);`); const result = await client.queryObject<{ x: number }>({ text: `SELECT X FROM MY_TEST`, fields: ["x"], @@ -119,8 +117,8 @@ Deno.test("INSERT works correctly", async () => { ### Setting up an advanced development environment -More advanced features such as the Deno inspector, test and permission -filtering, database inspection and test code lens can be achieved by setting up +More advanced features, such as the Deno inspector, test, and permission +filtering, database inspection, and test code lens can be achieved by setting up a local testing environment, as shown in the following steps: 1. Start the development databases using the Docker service with the command\ @@ -134,8 +132,9 @@ a local testing environment, as shown in the following steps: all environments The `DENO_POSTGRES_DEVELOPMENT` variable will tell the testing pipeline to - use the local testing settings specified in `tests/config.json`, instead of - the CI settings + use the local testing settings specified in `tests/config.json` instead of + the CI settings. + 3. Run the tests manually by using the command\ `deno test -A` @@ -143,35 +142,33 @@ a local testing environment, as shown in the following steps: 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 library - -This situation will become more stable as `std` and `deno-postgres` approach 1.0 - -| Deno version | Min driver version | Max driver version | -| ----------------------------------------------------- | ------------------ | ------------------ | -| 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.15.0 | 0.13.0 | | -| 1.16.0 | 0.14.0 | 0.14.3 | -| 1.17.0 | 0.15.0 | | -| 1.40.0 | 0.17.2 | | -| This module is available on JSR starting from 0.17.2. | | | +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 repository make sure to: +When contributing to the repository, make sure to: -1. All features and fixes must have an open issue in order to be discussed -2. All public interfaces must be typed and have a corresponding JS block +1. All features and fixes must have an open issue to be discussed +2. All public interfaces must be typed and have a corresponding JSDoc block explaining their usage 3. All code must pass the format and lint checks enforced by `deno fmt` and - `deno lint` respectively. The build will not pass the tests if these - conditions are not met. Ignore rules will be accepted in the code base when + `deno lint` respectively. The build will only pass the tests if these +conditions are met. Ignore rules will be accepted in the code base when their respective justification is given in a comment -4. All features and fixes must have a corresponding test added in order to be +4. All features and fixes must have a corresponding test added to be accepted ## Maintainers guidelines @@ -186,5 +183,4 @@ preserved their individual licenses and copyrights. Everything is licensed under the MIT License. -All additional work is copyright 2018 - 2022 — Bartłomiej Iwańczuk and Steven -Guerrero — All rights reserved. +All additional work is copyright 2018 - 2022 — Bartłomiej Iwańczuk and Steven Guerrero — All rights reserved. From c5f5b2dcfc33a523613581e02d8398ca059c6c52 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Wed, 7 Feb 2024 09:43:26 -0400 Subject: [PATCH 05/13] chore: fix docs formatting --- README.md | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index aaa40e4f..ec0f4fc2 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ await client.connect(); } { - const result = - await client.queryArray`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; + const result = await client + .queryArray`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; console.log(result.rows); // [[1, 'Carlos']] } @@ -43,8 +43,8 @@ await client.connect(); } { - const result = - await client.queryObject`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; + const result = await client + .queryObject`SELECT ID, NAME FROM PEOPLE WHERE ID = ${1}`; console.log(result.rows); // [{id: 1, name: 'Carlos'}] } @@ -59,8 +59,8 @@ For more examples, visit the documentation available at The documentation is available on the deno-postgres website [https://deno-postgres.com/](https://deno-postgres.com/) -Join the [Discord](https://discord.gg/HEdTCvZUSf) as well! It's a good place -to discuss bugs and features before opening issues. +Join the [Discord](https://discord.gg/HEdTCvZUSf) as well! It's a good place to +discuss bugs and features before opening issues. ## Contributing @@ -71,17 +71,16 @@ to discuss bugs and features before opening issues. - https://docs.docker.com/get-docker/ - https://docs.docker.com/compose/install/ -- You don't need `deno` installed in your machine to run the tests since it - will be installed in the Docker container when you build it. However, you will - need it to run the linter and formatter locally +- You don't need `deno` installed in your machine to run the tests since it will + be installed in the Docker container when you build it. However, you will need + it to run the linter and formatter locally - https://deno.land/ - `deno upgrade --version 1.7.1` - `dvm install 1.7.1 && dvm use 1.7.1` -- 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 +- 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 ### Running the tests @@ -97,8 +96,8 @@ The build step will check linting and formatting as well and report it to the command line 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 +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 @@ -166,10 +165,9 @@ When contributing to the repository, make sure to: explaining their usage 3. All code must pass the format and lint checks enforced by `deno fmt` and `deno lint` respectively. The build will only pass the tests if these -conditions are met. Ignore rules will be accepted in the code base when - their respective justification is given in a comment -4. All features and fixes must have a corresponding test added to be - accepted + conditions are met. Ignore rules will be accepted in the code base when their + respective justification is given in a comment +4. All features and fixes must have a corresponding test added to be accepted ## Maintainers guidelines @@ -183,4 +181,5 @@ preserved their individual licenses and copyrights. Everything is licensed under the MIT License. -All additional work is copyright 2018 - 2022 — Bartłomiej Iwańczuk and Steven Guerrero — All rights reserved. +All additional work is copyright 2018 - 2022 — Bartłomiej Iwańczuk and Steven +Guerrero — All rights reserved. From 27858553dc3c259c868193b5213dee7cf269d170 Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Sun, 11 Feb 2024 21:12:53 -0400 Subject: [PATCH 06/13] Add decode strategy control (#456) * feate: add encoding strategy control * chore: add encoding strategy tests * chore: fix file formatting * chore: fix lint issue of unused import * chore: fix variable anem to make camelcase --- connection/connection.ts | 4 +- connection/connection_params.ts | 39 ++++- query/decode.ts | 14 +- query/query.ts | 9 +- tests/config.ts | 16 +- tests/decode_test.ts | 77 +++++++++ tests/query_client_test.ts | 272 +++++++++++++++++--------------- 7 files changed, 287 insertions(+), 144 deletions(-) diff --git a/connection/connection.ts b/connection/connection.ts index e7278c6c..c062553c 100644 --- a/connection/connection.ts +++ b/connection/connection.ts @@ -685,7 +685,7 @@ export class Connection { case INCOMING_QUERY_MESSAGES.DATA_ROW: { const row_data = parseRowDataMessage(current_message); try { - result.insertRow(row_data); + result.insertRow(row_data, this.#connection_params.controls); } catch (e) { error = e; } @@ -862,7 +862,7 @@ export class Connection { case INCOMING_QUERY_MESSAGES.DATA_ROW: { const row_data = parseRowDataMessage(current_message); try { - result.insertRow(row_data); + result.insertRow(row_data, this.#connection_params.controls); } catch (e) { error = e; } diff --git a/connection/connection_params.ts b/connection/connection_params.ts index c3037736..bf006a21 100644 --- a/connection/connection_params.ts +++ b/connection/connection_params.ts @@ -91,19 +91,43 @@ export interface TLSOptions { caCertificates: string[]; } +/** + * Control the behavior for the client instance + */ +export type ClientControls = { + /** + * The strategy to use when decoding binary fields + * + * `string` : all values are returned as string, and the user has to take care of parsing + * `auto` : deno-postgres parses the data into JS objects (as many as possible implemented, non-implemented parsers would still return strings) + * + * Default: `auto` + * + * Future strategies might include: + * - `strict` : deno-postgres parses the data into JS objects, and if a parser is not implemented, it throws an error + * - `raw` : the data is returned as Uint8Array + */ + decodeStrategy?: "string" | "auto"; +}; + /** The Client database connection options */ export type ClientOptions = { /** Name of the application connecing to the database */ applicationName?: string; /** Additional connection options */ connection?: Partial; + /** Control the client behavior */ + controls?: ClientControls; /** The database name */ database?: string; /** The name of the host */ hostname?: string; /** The type of host connection */ host_type?: "tcp" | "socket"; - /** Additional client options */ + /** + * Additional connection URI options + * https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS + */ options?: string | Record; /** The database user password */ password?: string; @@ -118,14 +142,18 @@ export type ClientOptions = { /** The configuration options required to set up a Client instance */ export type ClientConfiguration = & Required< - Omit + Omit< + ClientOptions, + "password" | "port" | "tls" | "connection" | "options" | "controls" + > > & { + connection: ConnectionOptions; + controls?: ClientControls; + options: Record; password?: string; port: number; tls: TLSOptions; - connection: ConnectionOptions; - options: Record; }; function formatMissingParams(missingParams: string[]) { @@ -168,7 +196,7 @@ function assertRequiredOptions( // TODO // Support more options from the spec -/** options from URI per https://www.postgresql.org/docs/14/libpq-connect.html#LIBPQ-CONNSTRING */ +/** options from URI per https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING */ interface PostgresUri { application_name?: string; dbname?: string; @@ -447,6 +475,7 @@ export function createParams( caCertificates: params?.tls?.caCertificates ?? [], }, user: params.user ?? pgEnv.user, + controls: params.controls, }; assertRequiredOptions( diff --git a/query/decode.ts b/query/decode.ts index b09940d6..1afe82a0 100644 --- a/query/decode.ts +++ b/query/decode.ts @@ -35,6 +35,7 @@ import { decodeTid, decodeTidArray, } from "./decoders.ts"; +import { ClientControls } from "../connection/connection_params.ts"; export class Column { constructor( @@ -58,7 +59,7 @@ const decoder = new TextDecoder(); // TODO // Decode binary fields function decodeBinary() { - throw new Error("Not implemented!"); + throw new Error("Decoding binary data is not implemented!"); } function decodeText(value: Uint8Array, typeOid: number) { @@ -208,10 +209,19 @@ function decodeText(value: Uint8Array, typeOid: number) { } } -export function decode(value: Uint8Array, column: Column) { +export function decode( + value: Uint8Array, + column: Column, + controls?: ClientControls, +) { if (column.format === Format.BINARY) { return decodeBinary(); } else if (column.format === Format.TEXT) { + // If the user has specified a decode strategy, use that + if (controls?.decodeStrategy === "string") { + return decoder.decode(value); + } + // default to 'auto' mode, which uses the typeOid to determine the decoding strategy return decodeText(value, column.typeOid); } else { throw new Error(`Unknown column format: ${column.format}`); diff --git a/query/query.ts b/query/query.ts index 46f9b3c5..0bb39d7b 100644 --- a/query/query.ts +++ b/query/query.ts @@ -1,6 +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"; // TODO // Limit the type of parameters that can be passed @@ -242,7 +243,7 @@ export class QueryArrayResult< /** * Insert a row into the result */ - insertRow(row_data: Uint8Array[]) { + insertRow(row_data: Uint8Array[], controls?: ClientControls) { if (!this.rowDescription) { throw new Error( "The row descriptions required to parse the result data weren't initialized", @@ -256,7 +257,7 @@ export class QueryArrayResult< if (raw_value === null) { return null; } - return decode(raw_value, column); + return decode(raw_value, column, controls); }) as T; this.rows.push(row); @@ -303,7 +304,7 @@ export class QueryObjectResult< /** * Insert a row into the result */ - insertRow(row_data: Uint8Array[]) { + insertRow(row_data: Uint8Array[], controls?: ClientControls) { if (!this.rowDescription) { throw new Error( "The row description required to parse the result data wasn't initialized", @@ -364,7 +365,7 @@ export class QueryObjectResult< if (raw_value === null) { row[columns[index]] = null; } else { - row[columns[index]] = decode(raw_value, current_column); + row[columns[index]] = decode(raw_value, current_column, controls); } return row; diff --git a/tests/config.ts b/tests/config.ts index fbd2b45f..17bf701c 100644 --- a/tests/config.ts +++ b/tests/config.ts @@ -1,4 +1,7 @@ -import { ClientConfiguration } from "../connection/connection_params.ts"; +import { + ClientConfiguration, + ClientOptions, +} from "../connection/connection_params.ts"; import config_file1 from "./config.json" with { type: "json" }; type TcpConfiguration = Omit & { @@ -67,17 +70,20 @@ export const getClearSocketConfiguration = (): SocketConfiguration => { }; /** MD5 authenticated user with privileged access to the database */ -export const getMainConfiguration = (): TcpConfiguration => { +export const getMainConfiguration = ( + _config?: ClientOptions, +): TcpConfiguration => { return { applicationName: config.postgres_md5.applicationName, database: config.postgres_md5.database, hostname: config.postgres_md5.hostname, - host_type: "tcp", - options: {}, password: config.postgres_md5.password, + user: config.postgres_md5.users.main, + ..._config, + options: {}, port: config.postgres_md5.port, tls: enabled_tls, - user: config.postgres_md5.users.main, + host_type: "tcp", }; }; diff --git a/tests/decode_test.ts b/tests/decode_test.ts index 000cbab4..06512911 100644 --- a/tests/decode_test.ts +++ b/tests/decode_test.ts @@ -1,3 +1,4 @@ +import { Column, decode } from "../query/decode.ts"; import { decodeBigint, decodeBigintArray, @@ -17,6 +18,7 @@ import { decodeTid, } from "../query/decoders.ts"; import { assertEquals, assertThrows } from "./test_deps.ts"; +import { Oid } from "../query/oid.ts"; Deno.test("decodeBigint", function () { assertEquals(decodeBigint("18014398509481984"), 18014398509481984n); @@ -248,3 +250,78 @@ Deno.test("decodeTid", function () { 29383838509481984n, ]); }); + +Deno.test("decode strategy", function () { + const testValues = [ + { + value: "40", + column: new Column("test", 0, 0, Oid.int4, 0, 0, 0), + parsed: 40, + }, + { + value: "my_value", + column: new Column("test", 0, 0, Oid.text, 0, 0, 0), + parsed: "my_value", + }, + { + value: "[(100,50),(350,350)]", + column: new Column("test", 0, 0, Oid.path, 0, 0, 0), + parsed: [ + { x: "100", y: "50" }, + { x: "350", y: "350" }, + ], + }, + { + value: '{"value_1","value_2","value_3"}', + column: new Column("test", 0, 0, Oid.text_array, 0, 0, 0), + parsed: ["value_1", "value_2", "value_3"], + }, + { + value: "1997-12-17 07:37:16-08", + column: new Column("test", 0, 0, Oid.timestamp, 0, 0, 0), + parsed: new Date("1997-12-17 07:37:16-08"), + }, + { + value: "Yes", + column: new Column("test", 0, 0, Oid.bool, 0, 0, 0), + parsed: true, + }, + { + value: "<(12.4,2),3.5>", + column: new Column("test", 0, 0, Oid.circle, 0, 0, 0), + parsed: { point: { x: "12.4", y: "2" }, radius: "3.5" }, + }, + { + value: '{"test":1,"val":"foo","example":[1,2,false]}', + column: new Column("test", 0, 0, Oid.jsonb, 0, 0, 0), + parsed: { test: 1, val: "foo", example: [1, 2, false] }, + }, + { + value: "18014398509481984", + column: new Column("test", 0, 0, Oid.int8, 0, 0, 0), + parsed: 18014398509481984n, + }, + { + value: "{3.14,1.11,0.43,200}", + column: new Column("test", 0, 0, Oid.float4_array, 0, 0, 0), + parsed: [3.14, 1.11, 0.43, 200], + }, + ]; + + for (const testValue of testValues) { + const encodedValue = new TextEncoder().encode(testValue.value); + + // check default behavior + assertEquals(decode(encodedValue, testValue.column), testValue.parsed); + // check 'auto' behavior + assertEquals( + decode(encodedValue, testValue.column, { decodeStrategy: "auto" }), + testValue.parsed, + ); + // check 'string' behavior + assertEquals( + decode(encodedValue, testValue.column, { decodeStrategy: "string" }), + testValue.value, + ); + } +}); diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts index bd6c5014..4c4217bf 100644 --- a/tests/query_client_test.ts +++ b/tests/query_client_test.ts @@ -14,12 +14,14 @@ import { } from "./test_deps.ts"; import { getMainConfiguration } from "./config.ts"; import { PoolClient, QueryClient } from "../client.ts"; +import { ClientOptions } from "../connection/connection_params.ts"; function withClient( t: (client: QueryClient) => void | Promise, + config?: ClientOptions, ) { async function clientWrapper() { - const client = new Client(getMainConfiguration()); + const client = new Client(getMainConfiguration(config)); try { await client.connect(); await t(client); @@ -29,7 +31,7 @@ function withClient( } async function poolWrapper() { - const pool = new Pool(getMainConfiguration(), 1); + const pool = new Pool(getMainConfiguration(config), 1); let client; try { client = await pool.connect(); @@ -112,15 +114,56 @@ Deno.test( }), ); +Deno.test( + "Decode strategy - auto", + withClient( + async (client) => { + const result = await client.queryObject( + `SELECT ARRAY[1, 2, 3] AS _int_array, 3.14::REAL AS _float, 'DATA' AS _text, '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _json, 'Y'::BOOLEAN AS _bool`, + ); + + assertEquals(result.rows, [ + { + _bool: true, + _float: 3.14, + _int_array: [1, 2, 3], + _json: { test: "foo", arr: [1, 2, 3] }, + _text: "DATA", + }, + ]); + }, + { controls: { decodeStrategy: "auto" } }, + ), +); + +Deno.test( + "Decode strategy - string", + withClient( + async (client) => { + const result = await client.queryObject( + `SELECT ARRAY[1, 2, 3] AS _int_array, 3.14::REAL AS _float, 'DATA' AS _text, '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _json, 'Y'::BOOLEAN AS _bool`, + ); + + assertEquals(result.rows, [ + { + _bool: "t", + _float: "3.14", + _int_array: "{1,2,3}", + _json: '{"arr": [1, 2, 3], "test": "foo"}', + _text: "DATA", + }, + ]); + }, + { controls: { decodeStrategy: "string" } }, + ), +); + Deno.test( "Array arguments", withClient(async (client) => { { const value = "1"; - const result = await client.queryArray( - "SELECT $1", - [value], - ); + const result = await client.queryArray("SELECT $1", [value]); assertEquals(result.rows, [[value]]); } @@ -135,10 +178,7 @@ Deno.test( { const value = "3"; - const result = await client.queryObject( - "SELECT $1 AS ID", - [value], - ); + const result = await client.queryObject("SELECT $1 AS ID", [value]); assertEquals(result.rows, [{ id: value }]); } @@ -158,10 +198,7 @@ Deno.test( withClient(async (client) => { { const value = "1"; - const result = await client.queryArray( - "SELECT $id", - { id: value }, - ); + const result = await client.queryArray("SELECT $id", { id: value }); assertEquals(result.rows, [[value]]); } @@ -176,10 +213,9 @@ Deno.test( { const value = "3"; - const result = await client.queryObject( - "SELECT $id as ID", - { id: value }, - ); + const result = await client.queryObject("SELECT $id as ID", { + id: value, + }); assertEquals(result.rows, [{ id: value }]); } @@ -218,10 +254,9 @@ Deno.test( await client.queryArray`CREATE TEMP TABLE PREPARED_STATEMENT_ERROR (X INT)`; await assertRejects(() => - client.queryArray( - "INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", - ["TEXT"], - ) + client.queryArray("INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", [ + "TEXT", + ]) ); const { rows } = await client.queryObject<{ result: number }>({ @@ -237,10 +272,7 @@ Deno.test( "Array query can handle multiple query failures at once", withClient(async (client) => { await assertRejects( - () => - client.queryArray( - "SELECT 1; SELECT '2'::INT; SELECT 'A'::INT", - ), + () => client.queryArray("SELECT 1; SELECT '2'::INT; SELECT 'A'::INT"), PostgresError, "invalid input syntax for type integer", ); @@ -257,9 +289,7 @@ Deno.test( Deno.test( "Array query handles error during data processing", withClient(async (client) => { - await assertRejects( - () => client.queryObject`SELECT 'A' AS X, 'B' AS X`, - ); + await assertRejects(() => client.queryObject`SELECT 'A' AS X, 'B' AS X`); const value = "193"; const { rows: result_2 } = await client.queryObject`SELECT ${value} AS B`; @@ -292,11 +322,13 @@ Deno.test( withClient(async (client) => { await client.queryArray`CREATE TEMP TABLE PREPARED_STATEMENT_ERROR (X INT)`; - await assertRejects(() => - client.queryArray( - "INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", - ["TEXT"], - ), PostgresError); + await assertRejects( + () => + client.queryArray("INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", [ + "TEXT", + ]), + PostgresError, + ); const result = "handled"; @@ -313,9 +345,7 @@ Deno.test( Deno.test( "Prepared query handles error during data processing", withClient(async (client) => { - await assertRejects( - () => client.queryObject`SELECT ${1} AS A, ${2} AS A`, - ); + await assertRejects(() => client.queryObject`SELECT ${1} AS A, ${2} AS A`); const value = "z"; const { rows: result_2 } = await client.queryObject`SELECT ${value} AS B`; @@ -329,10 +359,10 @@ Deno.test( const item_1 = "Test;Azer"; const item_2 = "123;456"; - const { rows: result_1 } = await client.queryArray( - `SELECT ARRAY[$1, $2]`, - [item_1, item_2], - ); + const { rows: result_1 } = await client.queryArray(`SELECT ARRAY[$1, $2]`, [ + item_1, + item_2, + ]); assertEquals(result_1[0], [[item_1, item_2]]); }), ); @@ -441,10 +471,7 @@ Deno.test( text: `SELECT 1`, fields: ["res"], }); - assertEquals( - result[0].res, - 1, - ); + assertEquals(result[0].res, 1); assertEquals(client.connected, true); }), @@ -470,9 +497,7 @@ Deno.test( Deno.test( "Handling of query notices", withClient(async (client) => { - await client.queryArray( - "CREATE TEMP TABLE NOTICE_TEST (ABC INT);", - ); + await client.queryArray("CREATE TEMP TABLE NOTICE_TEST (ABC INT);"); const { warnings } = await client.queryArray( "CREATE TEMP TABLE IF NOT EXISTS NOTICE_TEST (ABC INT);", ); @@ -544,10 +569,9 @@ Deno.test( assertEquals(result_1[0][0], expectedBytes); - const { rows: result_2 } = await client.queryArray( - "SELECT $1::BYTEA", - [expectedBytes], - ); + const { rows: result_2 } = await client.queryArray("SELECT $1::BYTEA", [ + expectedBytes, + ]); assertEquals(result_2[0][0], expectedBytes); }), ); @@ -584,10 +608,9 @@ Deno.test( assertEquals(result.rowCount, 2); // parameterized delete - result = await client.queryArray( - "DELETE FROM METADATA WHERE VALUE = $1", - [300], - ); + result = await client.queryArray("DELETE FROM METADATA WHERE VALUE = $1", [ + 300, + ]); assertEquals(result.command, "DELETE"); assertEquals(result.rowCount, 1); @@ -626,7 +649,7 @@ Deno.test( `); assertEquals(result, [ - { "very_very_very_very_very_very_very_very_very_very_very_long_nam": 1 }, + { very_very_very_very_very_very_very_very_very_very_very_long_nam: 1 }, ]); assert(warnings[0].message.includes("will be truncated")); @@ -756,10 +779,7 @@ Deno.test( fields: ["a"], }); - assertEquals( - result_1[0].a, - 1, - ); + assertEquals(result_1[0].a, 1); await assertRejects( async () => { @@ -848,9 +868,10 @@ Deno.test( withClient(async (client) => { const value = { x: "A", y: "B" }; - const { rows } = await client.queryObject< - { x: string; y: string } - >`SELECT ${value.x} AS x, ${value.y} AS y`; + const { rows } = await client.queryObject<{ + x: string; + y: string; + }>`SELECT ${value.x} AS x, ${value.y} AS y`; assertEquals(rows[0], value); }), @@ -883,18 +904,18 @@ Deno.test( await transaction.queryArray`CREATE TEMP TABLE TEST (X INTEGER)`; const savepoint = await transaction.savepoint("table_creation"); await transaction.queryArray`INSERT INTO TEST (X) VALUES (1)`; - const query_1 = await transaction.queryObject< - { x: number } - >`SELECT X FROM TEST`; + const query_1 = await transaction.queryObject<{ + x: number; + }>`SELECT X FROM TEST`; assertEquals( query_1.rows[0].x, 1, "Operation was not executed inside transaction", ); await transaction.rollback(savepoint); - const query_2 = await transaction.queryObject< - { x: number } - >`SELECT X FROM TEST`; + const query_2 = await transaction.queryObject<{ + x: number; + }>`SELECT X FROM TEST`; assertEquals( query_2.rowCount, 0, @@ -953,21 +974,21 @@ Deno.test( await transaction_rr.begin(); // This locks the current value of the test table - await transaction_rr.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + await transaction_rr.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; // Modify data outside the transaction await client_2.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 2`; - const { rows: query_1 } = await client_2.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_1 } = await client_2.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals(query_1, [{ x: 2 }]); - const { rows: query_2 } = await transaction_rr.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_2 } = await transaction_rr.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_2, [{ x: 1 }], @@ -976,9 +997,9 @@ Deno.test( await transaction_rr.commit(); - const { rows: query_3 } = await client_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_3 } = await client_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_3, [{ x: 2 }], @@ -1007,9 +1028,9 @@ Deno.test( await transaction_rr.begin(); // This locks the current value of the test table - await transaction_rr.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + await transaction_rr.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; // Modify data outside the transaction await client_2.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 2`; @@ -1021,9 +1042,9 @@ Deno.test( "A serializable transaction should throw if the data read in the transaction has been modified externally", ); - const { rows: query_3 } = await client_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_3 } = await client_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_3, [{ x: 2 }], @@ -1064,23 +1085,22 @@ Deno.test( await client_1.queryArray`DROP TABLE IF EXISTS FOR_TRANSACTION_TEST`; await client_1.queryArray`CREATE TABLE FOR_TRANSACTION_TEST (X INTEGER)`; await client_1.queryArray`INSERT INTO FOR_TRANSACTION_TEST (X) VALUES (1)`; - const transaction_1 = client_1.createTransaction( - "transactionSnapshot1", - { isolation_level: "repeatable_read" }, - ); + const transaction_1 = client_1.createTransaction("transactionSnapshot1", { + isolation_level: "repeatable_read", + }); await transaction_1.begin(); // This locks the current value of the test table - await transaction_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + await transaction_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; // Modify data outside the transaction await client_2.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 2`; - const { rows: query_1 } = await transaction_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_1 } = await transaction_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_1, [{ x: 1 }], @@ -1089,15 +1109,15 @@ Deno.test( const snapshot = await transaction_1.getSnapshot(); - const transaction_2 = client_2.createTransaction( - "transactionSnapshot2", - { isolation_level: "repeatable_read", snapshot }, - ); + const transaction_2 = client_2.createTransaction("transactionSnapshot2", { + isolation_level: "repeatable_read", + snapshot, + }); await transaction_2.begin(); - const { rows: query_2 } = await transaction_2.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_2 } = await transaction_2.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_2, [{ x: 1 }], @@ -1170,9 +1190,9 @@ Deno.test( await transaction.begin(); await transaction.queryArray`INSERT INTO MY_TEST (X) VALUES (1)`; - const { rows: query_1 } = await transaction.queryObject< - { x: number } - >`SELECT X FROM MY_TEST`; + const { rows: query_1 } = await transaction.queryObject<{ + x: number; + }>`SELECT X FROM MY_TEST`; assertEquals(query_1, [{ x: 1 }]); await transaction.rollback({ chain: true }); @@ -1185,9 +1205,9 @@ Deno.test( await transaction.rollback(); - const { rowCount: query_2 } = await client.queryObject< - { x: number } - >`SELECT X FROM MY_TEST`; + const { rowCount: query_2 } = await client.queryObject<{ + x: number; + }>`SELECT X FROM MY_TEST`; assertEquals(query_2, 0); assertEquals( @@ -1250,31 +1270,31 @@ Deno.test( await transaction.begin(); await transaction.queryArray`CREATE TEMP TABLE X (Y INT)`; await transaction.queryArray`INSERT INTO X VALUES (1)`; - const { rows: query_1 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rows: query_1 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_1, [{ y: 1 }]); const savepoint = await transaction.savepoint(savepoint_name); await transaction.queryArray`DELETE FROM X`; - const { rowCount: query_2 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rowCount: query_2 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_2, 0); await savepoint.update(); await transaction.queryArray`INSERT INTO X VALUES (2)`; - const { rows: query_3 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rows: query_3 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_3, [{ y: 2 }]); await transaction.rollback(savepoint); - const { rowCount: query_4 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rowCount: query_4 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_4, 0); assertEquals( @@ -1291,9 +1311,9 @@ Deno.test( // This checks that the savepoint can be called by name as well await transaction.rollback(savepoint_name); - const { rows: query_5 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rows: query_5 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_5, [{ y: 1 }]); await transaction.commit(); From 5d90d8a125d8b854c365f529233753e88f18a250 Mon Sep 17 00:00:00 2001 From: Adam Zerner Date: Sun, 11 Feb 2024 18:08:47 -0800 Subject: [PATCH 07/13] "user experience" -> "developer experience" (#460) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec0f4fc2..0de12833 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Documentation](https://img.shields.io/github/v/release/denodrivers/postgres?color=yellow&label=Documentation&logo=deno&style=flat-square)](https://doc.deno.land/https/deno.land/x/postgres/mod.ts) [![License](https://img.shields.io/github/license/denodrivers/postgres?color=yellowgreen&label=License&style=flat-square)](LICENSE) -A lightweight PostgreSQL driver for Deno focused on user experience. +A lightweight PostgreSQL driver for Deno focused on developer experience. `deno-postgres` is being developed inspired by the excellent work of [node-postgres](https://github.com/brianc/node-postgres) and From cadd9a144134ff8abef57d4bb11f277feea713d8 Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Sun, 11 Feb 2024 22:41:40 -0400 Subject: [PATCH 08/13] Custom decoders (#461) * feate: add encoding strategy control * chore: add encoding strategy tests * chore: fix file formatting * chore: fix lint issue of unused import * feat: add custom parsers * chore: fix lint issues * chore: fix docs issue * chore: move custom decoder function inside try catch * chore: update code comments * chore: fix variable anem to make camelcase * chore: add Oid related types and rever Oid map to avoid iteration and keep performance * chore: update decoder tests to check type name, add presedence test * chore: update decoder types, update decode logic to check for custom decoders and strategy * chore: fix lint issue for const variable * docs: update code commetns and create documentation for results decoding * chore: update mode exports, fix jsdocs lint * chore: fix file formats --- README.md | 4 +- connection/connection_params.ts | 40 ++++++- docs/README.md | 178 ++++++++++++++++++++++-------- docs/index.html | 2 +- mod.ts | 4 + query/decode.ts | 102 ++++++++++-------- query/oid.ts | 184 +++++++++++++++++++++++++++++++- tests/query_client_test.ts | 134 ++++++++++++++++++++++- 8 files changed, 551 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 0de12833..d7e1adea 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,8 @@ discuss bugs and features before opening issues. it to run the linter and formatter locally - https://deno.land/ - - `deno upgrade --version 1.7.1` - - `dvm install 1.7.1 && dvm use 1.7.1` + - `deno upgrade --version 1.40.0` + - `dvm install 1.40.0 && dvm use 1.40.0` - 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 diff --git a/connection/connection_params.ts b/connection/connection_params.ts index bf006a21..ec4d07eb 100644 --- a/connection/connection_params.ts +++ b/connection/connection_params.ts @@ -1,6 +1,7 @@ import { parseConnectionUri } from "../utils/utils.ts"; import { ConnectionParamsError } from "../client/error.ts"; import { fromFileUrl, isAbsolute } from "../deps.ts"; +import { OidKey } from "../query/oid.ts"; /** * The connection string must match the following URI structure. All parameters but database and user are optional @@ -91,12 +92,23 @@ export interface TLSOptions { caCertificates: string[]; } +export type DecodeStrategy = "string" | "auto"; +export type Decoders = { + [key in number | OidKey]?: 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 + */ +export type DecoderFunction = (value: string, oid: number) => unknown; + /** * Control the behavior for the client instance */ export type ClientControls = { /** - * The strategy to use when decoding binary fields + * The strategy to use when decoding results data * * `string` : all values are returned as string, and the user has to take care of parsing * `auto` : deno-postgres parses the data into JS objects (as many as possible implemented, non-implemented parsers would still return strings) @@ -107,7 +119,31 @@ export type ClientControls = { * - `strict` : deno-postgres parses the data into JS objects, and if a parser is not implemented, it throws an error * - `raw` : the data is returned as Uint8Array */ - decodeStrategy?: "string" | "auto"; + decodeStrategy?: DecodeStrategy; + + /** + * 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 ClientControls.decodeStrategy}. Each key in the dictionary is the column OID type number, and the value is + * the decoder function. You can use the `Oid` object to set the decoder functions. + * + * @example + * ```ts + * import dayjs from 'https://esm.sh/dayjs'; + * 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), + * // 23 = Oid.int4 : convert all integers to positive numbers + * [Oid.int4]: (value: string) => Math.max(0, parseInt(value || '0', 10)), + * } + * } + * ``` + */ + decoders?: Decoders; }; /** The Client database connection options */ diff --git a/docs/README.md b/docs/README.md index 11eb512e..9b90bbff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,7 +53,7 @@ config = { host_type: "tcp", password: "password", options: { - "max_index_keys": "32", + max_index_keys: "32", }, port: 5432, user: "user", @@ -96,7 +96,7 @@ parsing the options in your connection string as if it was an options object You can create your own connection string by using the following structure: -``` +```txt driver://user:password@host:port/database_name driver://host:port/database_name?user=user&password=password&application_name=my_app @@ -126,6 +126,7 @@ of search parameters such as the following: - prefer: Attempt to stablish a TLS connection, default to unencrypted if the negotiation fails - disable: Skip TLS connection altogether + - user: If user is not specified in the url, this will be taken instead #### Password encoding @@ -438,13 +439,16 @@ For stronger management and scalability, you can use **pools**: ```ts const POOL_CONNECTIONS = 20; -const dbPool = new Pool({ - database: "database", - hostname: "hostname", - password: "password", - port: 5432, - user: "user", -}, POOL_CONNECTIONS); +const dbPool = new Pool( + { + database: "database", + hostname: "hostname", + password: "password", + port: 5432, + user: "user", + }, + POOL_CONNECTIONS, +); const client = await dbPool.connect(); // 19 connections are still available await client.queryArray`UPDATE X SET Y = 'Z'`; @@ -690,6 +694,101 @@ await client .queryArray`DELETE FROM ${my_table} WHERE MY_COLUMN = ${my_other_id};`; ``` +### Result decoding + +When a query is executed, the database returns all the data serialized as string +values. The `deno-postgres` driver automatically takes care of decoding the +results data of your query into the closest JavaScript compatible data type. +This makes it easy to work with the data in your applciation using native +Javascript types. A list of implemented type parsers can be found +[here](https://github.com/denodrivers/postgres/issues/446). + +However, you may have more specific needs or may want to handle decoding +yourself in your application. The driver provides 2 ways to handle decoding of +the result data: + +#### Decode strategy + +You can provide a global decode strategy to the client that will be used to +decode the result data. This can be done by setting the `decodeStrategy` +controls option when creating your query client. The following options are +available: + +- `auto` : (**default**) deno-postgres parses the data into JS types or objects + (non-implemented type parsers would still return strings). +- `string` : all values are returned as string, and the user has to take care of + parsing + +```ts +{ + // Will return all values parsed to native types + const client = new Client({ + database: "some_db", + user: "some_user", + controls: { + decodeStrategy: "auto", // or not setting it at all + }, + }); + + const result = await client.queryArray( + "SELECT ID, NAME, AGE, BIRTHDATE FROM PEOPLE WHERE ID = 1", + ); + console.log(result.rows); // [[1, "Laura", 25, 1996-01-01T00:00:00.000Z ]] + + // versus + + // Will return all values as strings + const client = new Client({ + database: "some_db", + user: "some_user", + controls: { + decodeStrategy: "string", + }, + }); + + const result = await client.queryArray( + "SELECT ID, NAME, AGE, BIRTHDATE FROM PEOPLE WHERE ID = 1", + ); + console.log(result.rows); // [["1", "Laura", "25", "1996-01-01"]] +} +``` + +#### Custom decoders + +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 options is a map object where the keys are the +type names or Oid number 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. + +```ts +{ + // Will return all values as strings, but custom decoders will take precedence + const client = new Client({ + database: "some_db", + user: "some_user", + controls: { + decodeStrategy: "string", + decoders: { + // Custom decoder for boolean + // for some reason, return booleans as an object with a type and value + bool: (value: string) => ({ + value: value === "t", + type: "boolean", + }), + }, + }, + }); + + const result = await client.queryObject( + "SELECT ID, NAME, IS_ACTIVE FROM PEOPLE", + ); + console.log(result.rows[0]); // {id: '1', name: 'Javier', _bool: { value: false, type: "boolean"}} +} +``` + ### Specifying result type Both the `queryArray` and `queryObject` functions have a generic implementation @@ -722,9 +821,10 @@ intellisense } { - const object_result = await client.queryObject< - { id: number; name: string } - >`SELECT ID, NAME FROM PEOPLE WHERE ID = ${17}`; + const object_result = await client.queryObject<{ + id: number; + name: string; + }>`SELECT ID, NAME FROM PEOPLE WHERE ID = ${17}`; // {id: number, name: string} const person = object_result.rows[0]; } @@ -741,9 +841,7 @@ interface User { name: string; } -const result = await client.queryObject( - "SELECT ID, NAME FROM PEOPLE", -); +const result = await client.queryObject("SELECT ID, NAME FROM PEOPLE"); // User[] const users = result.rows; @@ -791,12 +889,10 @@ To deal with this issue, it's recommended to provide a field list that maps to the expected properties we want in the resulting object ```ts -const result = await client.queryObject( - { - text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", - fields: ["id", "name"], - }, -); +const result = await client.queryObject({ + text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", + fields: ["id", "name"], +}); const users = result.rows; // [{id: 1, name: 'Ca'}, {id: 2, name: 'Jo'}, ...] ``` @@ -833,23 +929,19 @@ Other aspects to take into account when using the `fields` argument: ```ts { // This will throw because the property id is duplicated - await client.queryObject( - { - text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", - fields: ["id", "ID"], - }, - ); + await client.queryObject({ + text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", + fields: ["id", "ID"], + }); } { // This will throw because the returned number of columns don't match the // number of defined ones in the function call - await client.queryObject( - { - text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", - fields: ["id", "name", "something_else"], - }, - ); + await client.queryObject({ + text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", + fields: ["id", "name", "something_else"], + }); } ``` @@ -1078,6 +1170,7 @@ following levels of transaction isolation: - 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 const client_1 = await pool.connect(); const client_2 = await pool.connect(); @@ -1089,18 +1182,18 @@ following levels of transaction isolation: await transaction.begin(); // This locks the current value of IMPORTANT_TABLE // Up to this point, all other external changes will be included - const { rows: query_1 } = await transaction.queryObject< - { password: string } - >`SELECT PASSWORD FROM IMPORTANT_TABLE WHERE ID = ${my_id}`; + const { rows: query_1 } = await transaction.queryObject<{ + password: string; + }>`SELECT PASSWORD FROM IMPORTANT_TABLE WHERE ID = ${my_id}`; const password_1 = rows[0].password; // Concurrent operation executed by a different user in a different part of the code await client_2 .queryArray`UPDATE IMPORTANT_TABLE SET PASSWORD = 'something_else' WHERE ID = ${the_same_id}`; - const { rows: query_2 } = await transaction.queryObject< - { password: string } - >`SELECT PASSWORD FROM IMPORTANT_TABLE WHERE ID = ${my_id}`; + const { rows: query_2 } = await transaction.queryObject<{ + password: string; + }>`SELECT PASSWORD FROM IMPORTANT_TABLE WHERE ID = ${my_id}`; const password_2 = rows[0].password; // Database state is not updated while the transaction is ongoing @@ -1117,6 +1210,7 @@ following levels of transaction isolation: be visible until the transaction has finished. However this also 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 const client_1 = await pool.connect(); const client_2 = await pool.connect(); @@ -1128,9 +1222,9 @@ following levels of transaction isolation: await transaction.begin(); // This locks the current value of IMPORTANT_TABLE // Up to this point, all other external changes will be included - await transaction.queryObject< - { password: string } - >`SELECT PASSWORD FROM IMPORTANT_TABLE WHERE ID = ${my_id}`; + await transaction.queryObject<{ + password: string; + }>`SELECT PASSWORD FROM IMPORTANT_TABLE WHERE ID = ${my_id}`; // Concurrent operation executed by a different user in a different part of the code await client_2 diff --git a/docs/index.html b/docs/index.html index 066d193f..4ce33e9f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Deno Postgres - + diff --git a/mod.ts b/mod.ts index b0bac8ac..143abffc 100644 --- a/mod.ts +++ b/mod.ts @@ -5,14 +5,18 @@ 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 type { ClientOptions, ConnectionOptions, ConnectionString, + Decoders, + DecodeStrategy, TLSOptions, } from "./connection/connection_params.ts"; export type { Session } from "./client.ts"; diff --git a/query/decode.ts b/query/decode.ts index 1afe82a0..2904567d 100644 --- a/query/decode.ts +++ b/query/decode.ts @@ -1,4 +1,4 @@ -import { Oid } from "./oid.ts"; +import { Oid, OidType, OidTypes } from "./oid.ts"; import { bold, yellow } from "../deps.ts"; import { decodeBigint, @@ -62,9 +62,7 @@ function decodeBinary() { throw new Error("Decoding binary data is not implemented!"); } -function decodeText(value: Uint8Array, typeOid: number) { - const strValue = decoder.decode(value); - +function decodeText(value: string, typeOid: number) { try { switch (typeOid) { case Oid.bpchar: @@ -92,7 +90,7 @@ function decodeText(value: Uint8Array, typeOid: number) { case Oid.uuid: case Oid.varchar: case Oid.void: - return strValue; + return value; case Oid.bpchar_array: case Oid.char_array: case Oid.cidr_array: @@ -117,85 +115,85 @@ function decodeText(value: Uint8Array, typeOid: number) { case Oid.timetz_array: case Oid.uuid_array: case Oid.varchar_array: - return decodeStringArray(strValue); + return decodeStringArray(value); case Oid.float4: - return decodeFloat(strValue); + return decodeFloat(value); case Oid.float4_array: - return decodeFloatArray(strValue); + return decodeFloatArray(value); case Oid.int2: case Oid.int4: case Oid.xid: - return decodeInt(strValue); + return decodeInt(value); case Oid.int2_array: case Oid.int4_array: case Oid.xid_array: - return decodeIntArray(strValue); + return decodeIntArray(value); case Oid.bool: - return decodeBoolean(strValue); + return decodeBoolean(value); case Oid.bool_array: - return decodeBooleanArray(strValue); + return decodeBooleanArray(value); case Oid.box: - return decodeBox(strValue); + return decodeBox(value); case Oid.box_array: - return decodeBoxArray(strValue); + return decodeBoxArray(value); case Oid.circle: - return decodeCircle(strValue); + return decodeCircle(value); case Oid.circle_array: - return decodeCircleArray(strValue); + return decodeCircleArray(value); case Oid.bytea: - return decodeBytea(strValue); + return decodeBytea(value); case Oid.byte_array: - return decodeByteaArray(strValue); + return decodeByteaArray(value); case Oid.date: - return decodeDate(strValue); + return decodeDate(value); case Oid.date_array: - return decodeDateArray(strValue); + return decodeDateArray(value); case Oid.int8: - return decodeBigint(strValue); + return decodeBigint(value); case Oid.int8_array: - return decodeBigintArray(strValue); + return decodeBigintArray(value); case Oid.json: case Oid.jsonb: - return decodeJson(strValue); + return decodeJson(value); case Oid.json_array: case Oid.jsonb_array: - return decodeJsonArray(strValue); + return decodeJsonArray(value); case Oid.line: - return decodeLine(strValue); + return decodeLine(value); case Oid.line_array: - return decodeLineArray(strValue); + return decodeLineArray(value); case Oid.lseg: - return decodeLineSegment(strValue); + return decodeLineSegment(value); case Oid.lseg_array: - return decodeLineSegmentArray(strValue); + return decodeLineSegmentArray(value); case Oid.path: - return decodePath(strValue); + return decodePath(value); case Oid.path_array: - return decodePathArray(strValue); + return decodePathArray(value); case Oid.point: - return decodePoint(strValue); + return decodePoint(value); case Oid.point_array: - return decodePointArray(strValue); + return decodePointArray(value); case Oid.polygon: - return decodePolygon(strValue); + return decodePolygon(value); case Oid.polygon_array: - return decodePolygonArray(strValue); + return decodePolygonArray(value); case Oid.tid: - return decodeTid(strValue); + return decodeTid(value); case Oid.tid_array: - return decodeTidArray(strValue); + return decodeTidArray(value); case Oid.timestamp: case Oid.timestamptz: - return decodeDatetime(strValue); + return decodeDatetime(value); case Oid.timestamp_array: case Oid.timestamptz_array: - return decodeDatetimeArray(strValue); + return decodeDatetimeArray(value); default: // A separate category for not handled values // They might or might not be represented correctly as strings, // returning them to the user as raw strings allows them to parse // them as they see fit - return strValue; + return value; } } catch (_e) { console.error( @@ -214,15 +212,29 @@ export function decode( column: Column, controls?: ClientControls, ) { + const strValue = decoder.decode(value); + + // check if there is a custom decoder + if (controls?.decoders) { + // 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]]; + + if (decoderFunc) { + return decoderFunc(strValue, column.typeOid); + } + } + + // check if the decode strategy is `string` + if (controls?.decodeStrategy === "string") { + return strValue; + } + + // else, default to 'auto' mode, which uses the typeOid to determine the decoding strategy if (column.format === Format.BINARY) { return decodeBinary(); } else if (column.format === Format.TEXT) { - // If the user has specified a decode strategy, use that - if (controls?.decodeStrategy === "string") { - return decoder.decode(value); - } - // default to 'auto' mode, which uses the typeOid to determine the decoding strategy - return decodeText(value, column.typeOid); + return decodeText(strValue, column.typeOid); } else { throw new Error(`Unknown column format: ${column.format}`); } diff --git a/query/oid.ts b/query/oid.ts index 29fc63e5..9b36c88b 100644 --- a/query/oid.ts +++ b/query/oid.ts @@ -1,3 +1,9 @@ +export type OidKey = keyof typeof Oid; +export type OidType = (typeof Oid)[OidKey]; + +/** + * Oid is a map of OidKey to OidType. + */ export const Oid = { bool: 16, bytea: 17, @@ -166,4 +172,180 @@ export const Oid = { regnamespace_array: 4090, regrole: 4096, regrole_array: 4097, -}; +} as const; + +/** + * OidTypes is a map of OidType to OidKey. + * Used to decode values and avoid search iteration + */ +export const OidTypes: { + [key in OidType]: OidKey; +} = { + 16: "bool", + 17: "bytea", + 18: "char", + 19: "name", + 20: "int8", + 21: "int2", + 22: "_int2vector_0", + 23: "int4", + 24: "regproc", + 25: "text", + 26: "oid", + 27: "tid", + 28: "xid", + 29: "_cid_0", + 30: "_oidvector_0", + 32: "_pg_ddl_command", + 71: "_pg_type", + 75: "_pg_attribute", + 81: "_pg_proc", + 83: "_pg_class", + 114: "json", + 142: "_xml_0", + 143: "_xml_1", + 194: "_pg_node_tree", + 199: "json_array", + 210: "_smgr", + 325: "_index_am_handler", + 600: "point", + 601: "lseg", + 602: "path", + 603: "box", + 604: "polygon", + 628: "line", + 629: "line_array", + 650: "cidr", + 651: "cidr_array", + 700: "float4", + 701: "float8", + 702: "_abstime_0", + 703: "_reltime_0", + 704: "_tinterval_0", + 705: "_unknown", + 718: "circle", + 719: "circle_array", + 790: "_money_0", + 791: "_money_1", + 829: "macaddr", + 869: "inet", + 1000: "bool_array", + 1001: "byte_array", + 1002: "char_array", + 1003: "name_array", + 1005: "int2_array", + 1006: "_int2vector_1", + 1007: "int4_array", + 1008: "regproc_array", + 1009: "text_array", + 1010: "tid_array", + 1011: "xid_array", + 1012: "_cid_1", + 1013: "_oidvector_1", + 1014: "bpchar_array", + 1015: "varchar_array", + 1016: "int8_array", + 1017: "point_array", + 1018: "lseg_array", + 1019: "path_array", + 1020: "box_array", + 1021: "float4_array", + 1022: "float8_array", + 1023: "_abstime_1", + 1024: "_reltime_1", + 1025: "_tinterval_1", + 1027: "polygon_array", + 1028: "oid_array", + 1033: "_aclitem_0", + 1034: "_aclitem_1", + 1040: "macaddr_array", + 1041: "inet_array", + 1042: "bpchar", + 1043: "varchar", + 1082: "date", + 1083: "time", + 1114: "timestamp", + 1115: "timestamp_array", + 1182: "date_array", + 1183: "time_array", + 1184: "timestamptz", + 1185: "timestamptz_array", + 1186: "_interval_0", + 1187: "_interval_1", + 1231: "numeric_array", + 1248: "_pg_database", + 1263: "_cstring_0", + 1266: "timetz", + 1270: "timetz_array", + 1560: "_bit_0", + 1561: "_bit_1", + 1562: "_varbit_0", + 1563: "_varbit_1", + 1700: "numeric", + 1790: "_refcursor_0", + 2201: "_refcursor_1", + 2202: "regprocedure", + 2203: "regoper", + 2204: "regoperator", + 2205: "regclass", + 2206: "regtype", + 2207: "regprocedure_array", + 2208: "regoper_array", + 2209: "regoperator_array", + 2210: "regclass_array", + 2211: "regtype_array", + 2249: "_record_0", + 2275: "_cstring_1", + 2276: "_any", + 2277: "_anyarray", + 2278: "void", + 2279: "_trigger", + 2280: "_language_handler", + 2281: "_internal", + 2282: "_opaque", + 2283: "_anyelement", + 2287: "_record_1", + 2776: "_anynonarray", + 2842: "_pg_authid", + 2843: "_pg_auth_members", + 2949: "_txid_snapshot_0", + 2950: "uuid", + 2951: "uuid_array", + 2970: "_txid_snapshot_1", + 3115: "_fdw_handler", + 3220: "_pg_lsn_0", + 3221: "_pg_lsn_1", + 3310: "_tsm_handler", + 3500: "_anyenum", + 3614: "_tsvector_0", + 3615: "_tsquery_0", + 3642: "_gtsvector_0", + 3643: "_tsvector_1", + 3644: "_gtsvector_1", + 3645: "_tsquery_1", + 3734: "regconfig", + 3735: "regconfig_array", + 3769: "regdictionary", + 3770: "regdictionary_array", + 3802: "jsonb", + 3807: "jsonb_array", + 3831: "_anyrange", + 3838: "_event_trigger", + 3904: "_int4range_0", + 3905: "_int4range_1", + 3906: "_numrange_0", + 3907: "_numrange_1", + 3908: "_tsrange_0", + 3909: "_tsrange_1", + 3910: "_tstzrange_0", + 3911: "_tstzrange_1", + 3912: "_daterange_0", + 3913: "_daterange_1", + 3926: "_int8range_0", + 3927: "_int8range_1", + 4066: "_pg_shseclabel", + 4089: "regnamespace", + 4090: "regnamespace_array", + 4096: "regrole", + 4097: "regrole_array", +} as const; diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts index 4c4217bf..84e05f94 100644 --- a/tests/query_client_test.ts +++ b/tests/query_client_test.ts @@ -15,6 +15,7 @@ import { import { getMainConfiguration } from "./config.ts"; import { PoolClient, QueryClient } from "../client.ts"; import { ClientOptions } from "../connection/connection_params.ts"; +import { Oid } from "../query/oid.ts"; function withClient( t: (client: QueryClient) => void | Promise, @@ -119,7 +120,13 @@ Deno.test( withClient( async (client) => { const result = await client.queryObject( - `SELECT ARRAY[1, 2, 3] AS _int_array, 3.14::REAL AS _float, 'DATA' AS _text, '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _json, 'Y'::BOOLEAN AS _bool`, + `SELECT + 'Y'::BOOLEAN AS _bool, + 3.14::REAL AS _float, + ARRAY[1, 2, 3] AS _int_array, + '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _jsonb, + 'DATA' AS _text + ;`, ); assertEquals(result.rows, [ @@ -127,7 +134,7 @@ Deno.test( _bool: true, _float: 3.14, _int_array: [1, 2, 3], - _json: { test: "foo", arr: [1, 2, 3] }, + _jsonb: { test: "foo", arr: [1, 2, 3] }, _text: "DATA", }, ]); @@ -141,7 +148,13 @@ Deno.test( withClient( async (client) => { const result = await client.queryObject( - `SELECT ARRAY[1, 2, 3] AS _int_array, 3.14::REAL AS _float, 'DATA' AS _text, '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _json, 'Y'::BOOLEAN AS _bool`, + `SELECT + 'Y'::BOOLEAN AS _bool, + 3.14::REAL AS _float, + ARRAY[1, 2, 3] AS _int_array, + '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _jsonb, + 'DATA' AS _text + ;`, ); assertEquals(result.rows, [ @@ -149,7 +162,7 @@ Deno.test( _bool: "t", _float: "3.14", _int_array: "{1,2,3}", - _json: '{"arr": [1, 2, 3], "test": "foo"}', + _jsonb: '{"arr": [1, 2, 3], "test": "foo"}', _text: "DATA", }, ]); @@ -158,6 +171,119 @@ Deno.test( ), ); +Deno.test( + "Custom decoders", + withClient( + async (client) => { + const result = await client.queryObject( + `SELECT + 0::BOOLEAN AS _bool, + (DATE '2024-01-01' + INTERVAL '2 months')::DATE AS _date, + 7.90::REAL AS _float, + 100 AS _int, + '{"foo": "a", "bar": [1,2,3], "baz": null}'::JSONB AS _jsonb, + 'MY_VALUE' AS _text, + DATE '2024-10-01' + INTERVAL '2 years' - INTERVAL '2 months' AS _timestamp + ;`, + ); + + assertEquals(result.rows, [ + { + _bool: { boolean: false }, + _date: new Date("2024-03-03T00:00:00.000Z"), + _float: 785, + _int: 200, + _jsonb: { id: "999", foo: "A", bar: [2, 4, 6], baz: "initial" }, + _text: ["E", "U", "L", "A", "V", "_", "Y", "M"], + _timestamp: { year: 2126, month: "---08" }, + }, + ]); + }, + { + controls: { + decoders: { + // convert to object + [Oid.bool]: (value: string) => ({ boolean: value === "t" }), + // 1082 = date : convert to date and add 2 days + "1082": (value: string) => { + const d = new Date(value); + return new Date(d.setDate(d.getDate() + 2)); + }, + // multiply by 100 - 5 = 785 + float4: (value: string) => parseFloat(value) * 100 - 5, + // convert to int and add 100 = 200 + [Oid.int4]: (value: string) => parseInt(value, 10) + 100, + // parse with multiple conditions + jsonb: (value: string) => { + const obj = JSON.parse(value); + obj.foo = obj.foo.toUpperCase(); + obj.id = "999"; + obj.bar = obj.bar.map((v: number) => v * 2); + if (obj.baz === null) obj.baz = "initial"; + return obj; + }, + // split string and reverse + [Oid.text]: (value: string) => value.split("").reverse(), + // 1114 = timestamp : format timestamp into custom object + 1114: (value: string) => { + const d = new Date(value); + return { + year: d.getFullYear() + 100, + month: `---${d.getMonth() + 1 < 10 ? "0" : ""}${ + d.getMonth() + 1 + }`, + }; + }, + }, + }, + }, + ), +); + +Deno.test( + "Custom decoder precedence", + withClient( + async (client) => { + const result = await client.queryObject( + `SELECT + 0::BOOLEAN AS _bool, + 1 AS _int, + 1::REAL AS _float, + 'TEST' AS _text + ;`, + ); + + assertEquals(result.rows, [ + { + _bool: "success", + _float: "success", + _int: "success", + _text: "success", + }, + ]); + }, + { + controls: { + // numeric oid type values take precedence over name + decoders: { + // bool + bool: () => "fail", + [16]: () => "success", + //int + int4: () => "fail", + [Oid.int4]: () => "success", + // float4 + float4: () => "fail", + "700": () => "success", + // text + text: () => "fail", + 25: () => "success", + }, + }, + }, + ), +); + Deno.test( "Array arguments", withClient(async (client) => { From 184769a72c6c2f00bc38b0be47614397921d40c4 Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Sun, 11 Feb 2024 22:56:17 -0400 Subject: [PATCH 09/13] Update package version (#462) * chore: update package version * chore: update readme credits * update readme credits date --- README.md | 4 ++-- deno.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d7e1adea..41ce9e4f 100644 --- a/README.md +++ b/README.md @@ -181,5 +181,5 @@ preserved their individual licenses and copyrights. Everything is licensed under the MIT License. -All additional work is copyright 2018 - 2022 — Bartłomiej Iwańczuk and Steven -Guerrero — All rights reserved. +All additional work is copyright 2018 - 2024 — Bartłomiej Iwańczuk, Steven +Guerrero, Hector Ayala — All rights reserved. diff --git a/deno.json b/deno.json index 1fc619c0..a25df52d 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "lock": false, "name": "@bartlomieju/postgres", - "version": "0.17.2", + "version": "0.18.0", "exports": "./mod.ts" } From 33c2181de909fdeda9ff4aea2c2d0a2bcb582bf3 Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Sun, 11 Feb 2024 23:01:53 -0400 Subject: [PATCH 10/13] Update README.md --- docs/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/README.md b/docs/README.md index 9b90bbff..9c0953a5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -687,7 +687,7 @@ statements apply here as well const my_id = 17; await client.queryArray`UPDATE TABLE X SET Y = 0 WHERE Z = ${my_id}`; -// Invalid attempt to replace an specifier +// Invalid attempt to replace a specifier const my_table = "IMPORTANT_TABLE"; const my_other_id = 41; await client @@ -699,12 +699,12 @@ await client When a query is executed, the database returns all the data serialized as string values. The `deno-postgres` driver automatically takes care of decoding the results data of your query into the closest JavaScript compatible data type. -This makes it easy to work with the data in your applciation using native +This makes it easy to work with the data in your application using native Javascript types. A list of implemented type parsers can be found [here](https://github.com/denodrivers/postgres/issues/446). However, you may have more specific needs or may want to handle decoding -yourself in your application. The driver provides 2 ways to handle decoding of +yourself in your application. The driver provides two ways to handle decoding of the result data: #### Decode strategy @@ -714,9 +714,9 @@ decode the result data. This can be done by setting the `decodeStrategy` controls option when creating your query client. The following options are available: -- `auto` : (**default**) deno-postgres parses the data into JS types or objects +- `auto`: (**default**) deno-postgres parses the data into JS types or objects (non-implemented type parsers would still return strings). -- `string` : all values are returned as string, and the user has to take care of +- `string`: all values are returned as string, and the user has to take care of parsing ```ts @@ -733,7 +733,7 @@ available: const result = await client.queryArray( "SELECT ID, NAME, AGE, BIRTHDATE FROM PEOPLE WHERE ID = 1", ); - console.log(result.rows); // [[1, "Laura", 25, 1996-01-01T00:00:00.000Z ]] + console.log(result.rows); // [[1, "Laura", 25, Date('1996-01-01') ]] // versus @@ -757,8 +757,8 @@ 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 options is a map object where the keys are the -type names or Oid number and the values are the custom decoder functions. +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. You can use it with the decode strategy. Custom decoders take precedence over the strategy and internal parsers. @@ -785,7 +785,7 @@ 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', _bool: { value: false, type: "boolean"}} + console.log(result.rows[0]); // {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}} } ``` From a16967f97abfc8b42b68836b7101f3823e3fb72a Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Sun, 11 Feb 2024 23:13:38 -0400 Subject: [PATCH 11/13] Update README.md Fix grammatical errors --- docs/README.md | 128 ++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/docs/README.md b/docs/README.md index 9c0953a5..87efbbbd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ `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. +queries, prepared statements, connection pools, and transactions. ```ts import { Client } from "https://deno.land/x/postgres/mod.ts"; @@ -73,9 +73,9 @@ await client.end(); ### Connection defaults -The only required parameters for stablishing connection with your database are +The only required parameters for establishing connection with your database are the database name and your user, the rest of them have sensible defaults to save -up time when configuring your connection, such as the following: +uptime when configuring your connection, such as the following: - connection.attempts: "1" - connection.interval: Exponential backoff increasing the time by 500 ms on @@ -92,7 +92,7 @@ up time when configuring your connection, such as the following: Many services provide a connection string as a global format to connect to your database, and `deno-postgres` makes it easy to integrate this into your code by -parsing the options in your connection string as if it was an options object +parsing the options in your connection string as if it were an options object You can create your own connection string by using the following structure: @@ -116,14 +116,14 @@ of search parameters such as the following: - options: This parameter can be used by other database engines usable through 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 +- sslmode: Allows you to specify the tls configuration for your client; the allowed values are the following: - - verify-full: Same behaviour as `require` - - verify-ca: Same behaviour as `require` - - require: Attempt to stablish a TLS connection, abort the connection if the + - verify-full: Same behavior as `require` + - verify-ca: Same behavior as `require` + - require: Attempt to establish a TLS connection, abort the connection if the negotiation fails - - prefer: Attempt to stablish a TLS connection, default to unencrypted if the + - prefer: Attempt to establish a TLS connection, default to unencrypted if the negotiation fails - disable: Skip TLS connection altogether @@ -132,7 +132,7 @@ of search parameters such as the following: #### Password encoding One thing that must be taken into consideration is that passwords contained -inside the URL must be properly encoded in order to be passed down to the +inside the URL must be properly encoded to be passed down to the database. You can achieve that by using the JavaScript API `encodeURIComponent` and passing your password as an argument. @@ -146,17 +146,17 @@ and passing your password as an argument. - `postgres://me:Mtx%253@localhost:5432/my_database` - `postgres://me:p%C3%A1ssword!%3Dwith_symbols@localhost:5432/my_database` -If the password is not encoded correctly, the driver will try and pass the raw -password to the database, however it's highly recommended that all passwords are +If the password is not encoded correctly, the driver will try to pass the raw +password to the database, however, it's highly recommended that all passwords are always encoded to prevent authentication errors ### Database reconnection It's a very common occurrence to get broken connections due to connectivity -issues or OS related problems, however while this may be a minor inconvenience +issues or OS-related problems; however, while this may be a minor inconvenience in development, it becomes a serious matter in a production environment if not handled correctly. To mitigate the impact of disconnected clients -`deno-postgres` allows the developer to stablish a new connection with the +`deno-postgres` allows the developer to establish a new connection with the database automatically before executing a query on a broken connection. To manage the number of reconnection attempts, adjust the `connection.attempts` @@ -175,7 +175,7 @@ try { await client.queryArray`SELECT 1`; ``` -If automatic reconnection is not desired, the developer can simply set the +If automatic reconnection is not desired, the developer can set the number of attempts to zero and manage connection and reconnection manually ```ts @@ -202,9 +202,9 @@ Your initial connection will also be affected by this setting in a slightly different manner than already active errored connections. If you fail to connect to your database in the first attempt, the client will keep trying to connect as many times as requested, meaning that if your attempt configuration is three, -your total first-connection-attempts will ammount to four. +your total first-connection-attempts will amount to four. -Additionally you can set an interval before each reconnection by using the +Additionally, you can set an interval before each reconnection by using the `interval` parameter. This can be either a plane number or a function where the developer receives the previous interval and returns the new one, making it easy to implement exponential backoff (Note: the initial interval for this function @@ -305,7 +305,7 @@ const client = new Client( ); ``` -Additionally you can specify the host using the `host` URL parameter +Additionally, you can specify the host using the `host` URL parameter ```ts const client = new Client( @@ -325,15 +325,15 @@ terminate the connection or to attempt to connect using a non-encrypted one. This behavior can be defined using the connection parameter `tls.enforce` or the "required" option when using a connection string. -If set, the driver will fail inmediately if no TLS connection can be -established, otherwise the driver will attempt to connect without encryption -after TLS connection has failed, but will display a warning containing the +If set, the driver will fail immediately if no TLS connection can be +established, otherwise, the driver will attempt to connect without encryption +after the TLS connection has failed, but will display a warning containing the reason why the TLS connection failed. **This is the default configuration**. If you wish to skip TLS connections altogether, you can do so by passing false as a parameter in the `tls.enabled` option or the "disable" option when using a connection string. Although discouraged, this option is pretty useful when -dealing with development databases or versions of Postgres that didn't support +dealing with development databases or versions of Postgres that don't support TLS encrypted connections. #### About invalid and custom TLS certificates @@ -342,7 +342,7 @@ There is a myriad of factors you have to take into account when using a 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 +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) @@ -365,14 +365,14 @@ const client = new Client({ ``` TLS can be disabled from your server by editing your `postgresql.conf` file and -setting the `ssl` option to `off`, or in the driver side by using the "disabled" +setting the `ssl` option to `off`, or on the driver side by using the "disabled" option in the client configuration. ### Env parameters The values required to connect to the database can be read directly from environmental variables, given the case that the user doesn't provide them while -initializing the client. The only requirement for this variables to be read is +initializing the client. The only requirement for these variables to be read is for Deno to be run with `--allow-env` permissions The env variables that the client will recognize are taken from `libpq` to keep @@ -391,9 +391,9 @@ await client.end(); ## Connection Client Clients are the most basic block for establishing communication with your -database. They provide abstractions over queries, transactions and connection +database. They provide abstractions over queries, transactions, and connection management. In `deno-postgres`, similar clients such as the transaction and pool -client inherit it's functionality from the basic client, so the available +client inherit their functionality from the basic client, so the available methods will be very similar across implementations. You can create a new client by providing the required connection parameters: @@ -427,7 +427,7 @@ await client_1.end(); await client_2.end(); ``` -Ending a client will cause it to destroy it's connection with the database, +Ending a client will cause it to destroy its connection with the database, forcing you to reconnect in order to execute operations again. In Postgres, connections are a synonym for session, which means that temporal operations such as the creation of temporal tables or the use of the `PG_TEMP` schema will not @@ -515,7 +515,7 @@ await client_3.release(); #### Pools made simple -The following example is a simple abstraction over pools that allow you to +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 @@ -538,8 +538,8 @@ await runQuery("SELECT ID, NAME FROM USERS WHERE ID = '1'"); // [{id: 1, name: ' ## Executing queries Executing a query is as simple as providing the raw SQL to your client, it will -automatically be queued, validated and processed so you can get a human -readable, blazing fast result +automatically be queued, validated, and processed so you can get a human +readable, blazing-fast result ```ts const result = await client.queryArray("SELECT ID, NAME FROM PEOPLE"); @@ -552,7 +552,7 @@ Prepared statements are a Postgres mechanism designed to prevent SQL injection and maximize query performance for multiple queries (see https://security.stackexchange.com/questions/15214/are-prepared-statements-100-safe-against-sql-injection) -The idea is simple, provide a base sql statement with placeholders for any +The idea is simple, provide a base SQL statement with placeholders for any variables required, and then provide said variables in an array of arguments ```ts @@ -597,7 +597,7 @@ replaced at runtime with an argument object } ``` -Behind the scenes, `deno-postgres` will replace the variables names in your +Behind the scenes, `deno-postgres` will replace the variable names in your query for Postgres-readable placeholders making it easy to reuse values in multiple places in your query @@ -626,7 +626,7 @@ arguments object #### Template strings -Even thought the previous call is already pretty simple, it can be simplified +Even though the previous call is already pretty simple, it can be simplified even further by the use of template strings, offering all the benefits of prepared statements with a nice and clear syntax for your queries @@ -648,12 +648,12 @@ prepared statements with a nice and clear syntax for your queries Obviously, you can't pass any parameters provided by the `QueryOptions` interface such as explicitly named fields, so this API is best used when you -have a straight forward statement that only requires arguments to work as +have a straightforward statement that only requires arguments to work as intended -#### Regarding non argument parameters +#### Regarding non-argument parameters -A common assumption many people do when working with prepared statements is that +A common assumption many people make when working with prepared statements is that they work the same way string interpolation works, by replacing the placeholders with whatever variables have been passed down to the query. However the reality is a little more complicated than that where only very specific parts of a query @@ -676,7 +676,7 @@ SELECT MY_DATA FROM $1 Specifically, you can't replace any keyword or specifier in a query, only literal values, such as the ones you would use in an `INSERT` or `WHERE` clause -This is specially hard to grasp when working with template strings, since the +This is especially hard to grasp when working with template strings, since the assumption that is made most of the time is that all items inside a template string call are being interpolated with the underlying string, however as explained above this is not the case, so all previous warnings about prepared @@ -700,7 +700,7 @@ When a query is executed, the database returns all the data serialized as string values. The `deno-postgres` driver automatically takes care of decoding the results data of your query into the closest JavaScript compatible data type. This makes it easy to work with the data in your application using native -Javascript types. A list of implemented type parsers can be found +JavaScript types. A list of implemented type parsers can be found [here](https://github.com/denodrivers/postgres/issues/446). However, you may have more specific needs or may want to handle decoding @@ -714,7 +714,7 @@ decode the result data. This can be done by setting the `decodeStrategy` controls option when creating your query client. The following options are available: -- `auto`: (**default**) deno-postgres parses the data into JS types or objects +- `auto`: (**default**) values are parsed to JavaScript types or objects (non-implemented type parsers would still return strings). - `string`: all values are returned as string, and the user has to take care of parsing @@ -793,7 +793,7 @@ the strategy and internal parsers. Both the `queryArray` and `queryObject` functions have a generic implementation that allows users to type the result of the executed query to obtain -intellisense +IntelliSense ```ts { @@ -849,11 +849,11 @@ const users = result.rows; #### Case transformation -When consuming a database, specially one not managed by themselves but a +When consuming a database, especially one not managed by themselves but a external one, many developers have to face different naming standards that may disrupt the consistency of their codebase. And while there are simple solutions for that such as aliasing every query field that is done to the database, one -easyb built-in solution allows developers to transform the incoming query names +easy built-in solution allows developers to transform the incoming query names into the casing of their preference without any extra steps ##### Camelcase @@ -897,7 +897,7 @@ const result = await client.queryObject({ const users = result.rows; // [{id: 1, name: 'Ca'}, {id: 2, name: 'Jo'}, ...] ``` -**Don't use TypeScript generics to map these properties**, this generics only +**Don't use TypeScript generics to map these properties**, these generics only exist at compile time and won't affect the final outcome of the query ```ts @@ -936,7 +936,7 @@ Other aspects to take into account when using the `fields` argument: } { - // This will throw because the returned number of columns don't match the + // This will throw because the returned number of columns doesn't match the // number of defined ones in the function call await client.queryObject({ text: "SELECT ID, SUBSTR(NAME, 0, 2) FROM PEOPLE", @@ -949,7 +949,7 @@ Other aspects to take into account when using the `fields` argument: A lot of effort was put into abstracting Transactions in the library, and the final result is an API that is both simple to use and offers all of the options -and features that you would get by executing SQL statements, plus and extra +and features that you would get by executing SQL statements, plus an extra layer of abstraction that helps you catch mistakes ahead of time. #### Creating a transaction @@ -973,14 +973,14 @@ await transaction.commit(); ##### Transaction locks -Due to how SQL transactions work, everytime you begin a transaction all queries +Due to how SQL transactions work, every time you begin a transaction all queries you do in your session will run inside that transaction context. This is a problem for query execution since it might cause queries that are meant to do persistent changes to the database to live inside this context, making them -susceptible to be rolled back unintentionally. We will call this kind of queries +susceptible to being rolled back unintentionally. We will call this kind of queries **unsafe operations**. -Everytime you create a transaction the client you use will get a lock, with the +Every time you create a transaction the client you use will get a lock, with the purpose of blocking any external queries from running while a transaction takes course, effectively avoiding all unsafe operations. @@ -998,10 +998,10 @@ await transaction.commit(); await client.queryArray`DELETE TABLE X`; ``` -For this very reason however, if you are using transactions in an application +For this very reason, however, if you are using transactions in an application with concurrent access like an API, it is recommended that you don't use the Client API at all. If you do so, the client will be blocked from executing other -queries until the transaction has finished. Instead of that, use a connection +queries until the transaction has finished. Instead, use a connection pool, that way all your operations will be executed in a different context without locking the main client. @@ -1038,7 +1038,7 @@ SELECT ID FROM MY_TABLE; -- Will attempt to execute, but will fail cause transac COMMIT; -- Transaction will end, but no changes to MY_TABLE will be made ``` -However, due to how JavaScript works we can handle this kinds of errors in a +However, due to how JavaScript works we can handle these kinds of errors in a more fashionable way. All failed queries inside a transaction will automatically end it and release the main client. @@ -1055,7 +1055,7 @@ function executeMyTransaction() { await transaction.queryArray`SELECT []`; // Error will be thrown, transaction will be aborted await transaction.queryArray`SELECT ID FROM MY_TABLE`; // Won't even attempt to execute - await transaction.commit(); // Don't even need it, transaction was already ended + await transaction.commit(); // Don't even need it, the transaction was already ended } catch (e) { return false; } @@ -1064,9 +1064,9 @@ function executeMyTransaction() { } ``` -This limits only to database related errors though, regular errors won't end the +This limits only to database-related errors though, regular errors won't end the connection and may allow the user to execute a different code path. This is -specially good for ahead of time validation errors such as the ones found in the +especially good for ahead-of-time validation errors such as the ones found in the rollback and savepoint features. ```ts @@ -1098,7 +1098,7 @@ await transaction.commit(); #### Transaction options PostgreSQL provides many options to customize the behavior of transactions, such -as isolation level, read modes and startup snapshot. All this options can be set +as isolation level, read modes, and startup snapshot. All these options can be set by passing a second argument to the `startTransaction` method ```ts @@ -1116,10 +1116,10 @@ place _after_ the transaction had begun. The following is a demonstration. A sensible transaction that loads a table with some very important test results and the students that passed said test. This is -a long running operation, and in the meanwhile someone is tasked to cleanup the -results from the tests table because it's taking too much space in the database. +a long-running operation, and in the meanwhile, someone is tasked to clean up the +results from the tests table because it's taking up too much space in the database. -If the transaction were to be executed as it follows, the test results would be +If the transaction were to be executed as follows, the test results would be lost before the graduated students could be extracted from the original table, causing a mismatch in the data. @@ -1146,7 +1146,7 @@ await transaction.queryArray`INSERT INTO TEST_RESULTS // executes this query while the operation above still takes place await client_2.queryArray`DELETE FROM TESTS WHERE TEST_TYPE = 'final_test'`; -// Test information is gone, no data will be loaded into the graduated students table +// Test information is gone, and no data will be loaded into the graduated students table await transaction.queryArray`INSERT INTO GRADUATED_STUDENTS SELECT USER_ID @@ -1207,7 +1207,7 @@ following levels of transaction isolation: ``` - Serializable: Just like the repeatable read mode, all external changes won't - be visible until the transaction has finished. However this also prevents the + be visible until the transaction has finished. However, this also prevents the current transaction from making persistent changes if the data they were reading at the beginning of the transaction has been modified (recommended) @@ -1244,9 +1244,9 @@ following levels of transaction isolation: ##### Read modes -In many cases, and specially when allowing third parties to access data inside +In many cases, and especially when allowing third parties to access data inside your database it might be a good choice to prevent queries from modifying the -database in the course of the transaction. You can revoke this write privileges +database in the course of the transaction. You can revoke these write privileges by setting `read_only: true` in the transaction options. The default for all transactions will be to enable write permission. @@ -1357,7 +1357,7 @@ await transaction.rollback(savepoint); // Truncate gets undone ##### Rollback A rollback allows the user to end the transaction without persisting the changes -made to the database, preventing that way any unwanted operation to take place. +made to the database, preventing that way any unwanted operation from taking place. ```ts const transaction = client.createTransaction("rolled_back_transaction"); From 32e9e4363c5ca4667bdb66a26ed7785c35706e4c Mon Sep 17 00:00:00 2001 From: Hector Ayala Date: Sun, 11 Feb 2024 23:14:38 -0400 Subject: [PATCH 12/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41ce9e4f..17859ea7 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ discuss bugs and features before opening issues. - `dvm install 1.40.0 && dvm use 1.40.0` - 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 + library; it will run as a service in the Docker container when you build it ### Running the tests From a3623609b424203e12fec90214823bf33e4ba2a1 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Sun, 11 Feb 2024 23:16:10 -0400 Subject: [PATCH 13/13] chore: fix docs formatting --- docs/README.md | 60 ++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/README.md b/docs/README.md index 87efbbbd..528c2d25 100644 --- a/docs/README.md +++ b/docs/README.md @@ -132,9 +132,9 @@ of search parameters such as the following: #### Password encoding One thing that must be taken into consideration is that passwords contained -inside the URL must be properly encoded to be passed down to the -database. You can achieve that by using the JavaScript API `encodeURIComponent` -and passing your password as an argument. +inside the URL must be properly encoded to be passed down to the database. You +can achieve that by using the JavaScript API `encodeURIComponent` and passing +your password as an argument. **Invalid**: @@ -147,8 +147,8 @@ and passing your password as an argument. - `postgres://me:p%C3%A1ssword!%3Dwith_symbols@localhost:5432/my_database` If the password is not encoded correctly, the driver will try to pass the raw -password to the database, however, it's highly recommended that all passwords are -always encoded to prevent authentication errors +password to the database, however, it's highly recommended that all passwords +are always encoded to prevent authentication errors ### Database reconnection @@ -175,8 +175,8 @@ try { await client.queryArray`SELECT 1`; ``` -If automatic reconnection is not desired, the developer can set the -number of attempts to zero and manage connection and reconnection manually +If automatic reconnection is not desired, the developer can set the number of +attempts to zero and manage connection and reconnection manually ```ts const client = new Client({ @@ -597,9 +597,9 @@ replaced at runtime with an argument object } ``` -Behind the scenes, `deno-postgres` will replace the variable names in your -query for Postgres-readable placeholders making it easy to reuse values in -multiple places in your query +Behind the scenes, `deno-postgres` will replace the variable names in your query +for Postgres-readable placeholders making it easy to reuse values in multiple +places in your query ```ts { @@ -653,11 +653,11 @@ intended #### Regarding non-argument parameters -A common assumption many people make when working with prepared statements is that -they work the same way string interpolation works, by replacing the placeholders -with whatever variables have been passed down to the query. However the reality -is a little more complicated than that where only very specific parts of a query -can use placeholders to indicate upcoming values +A common assumption many people make when working with prepared statements is +that they work the same way string interpolation works, by replacing the +placeholders with whatever variables have been passed down to the query. However +the reality is a little more complicated than that where only very specific +parts of a query can use placeholders to indicate upcoming values That's the reason why the following works @@ -949,8 +949,8 @@ Other aspects to take into account when using the `fields` argument: A lot of effort was put into abstracting Transactions in the library, and the final result is an API that is both simple to use and offers all of the options -and features that you would get by executing SQL statements, plus an extra -layer of abstraction that helps you catch mistakes ahead of time. +and features that you would get by executing SQL statements, plus an extra layer +of abstraction that helps you catch mistakes ahead of time. #### Creating a transaction @@ -977,8 +977,8 @@ Due to how SQL transactions work, every time you begin a transaction all queries you do in your session will run inside that transaction context. This is a problem for query execution since it might cause queries that are meant to do persistent changes to the database to live inside this context, making them -susceptible to being rolled back unintentionally. We will call this kind of queries -**unsafe operations**. +susceptible to being rolled back unintentionally. We will call this kind of +queries **unsafe operations**. Every time you create a transaction the client you use will get a lock, with the purpose of blocking any external queries from running while a transaction takes @@ -1001,9 +1001,9 @@ await client.queryArray`DELETE TABLE X`; For this very reason, however, if you are using transactions in an application with concurrent access like an API, it is recommended that you don't use the Client API at all. If you do so, the client will be blocked from executing other -queries until the transaction has finished. Instead, use a connection -pool, that way all your operations will be executed in a different context -without locking the main client. +queries until the transaction has finished. Instead, use a connection pool, that +way all your operations will be executed in a different context without locking +the main client. ```ts const client_1 = await pool.connect(); @@ -1066,8 +1066,8 @@ function executeMyTransaction() { This limits only to database-related errors though, regular errors won't end the connection and may allow the user to execute a different code path. This is -especially good for ahead-of-time validation errors such as the ones found in the -rollback and savepoint features. +especially good for ahead-of-time validation errors such as the ones found in +the rollback and savepoint features. ```ts const transaction = client.createTransaction("abortable"); @@ -1098,8 +1098,8 @@ await transaction.commit(); #### Transaction options PostgreSQL provides many options to customize the behavior of transactions, such -as isolation level, read modes, and startup snapshot. All these options can be set -by passing a second argument to the `startTransaction` method +as isolation level, read modes, and startup snapshot. All these options can be +set by passing a second argument to the `startTransaction` method ```ts const transaction = client.createTransaction("ts_1", { @@ -1116,8 +1116,9 @@ place _after_ the transaction had begun. The following is a demonstration. A sensible transaction that loads a table with some very important test results and the students that passed said test. This is -a long-running operation, and in the meanwhile, someone is tasked to clean up the -results from the tests table because it's taking up too much space in the database. +a long-running operation, and in the meanwhile, someone is tasked to clean up +the results from the tests table because it's taking up too much space in the +database. If the transaction were to be executed as follows, the test results would be lost before the graduated students could be extracted from the original table, @@ -1357,7 +1358,8 @@ await transaction.rollback(savepoint); // Truncate gets undone ##### Rollback A rollback allows the user to end the transaction without persisting the changes -made to the database, preventing that way any unwanted operation from taking place. +made to the database, preventing that way any unwanted operation from taking +place. ```ts const transaction = client.createTransaction("rolled_back_transaction");