Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle array type custom decoders #470

Merged
merged 7 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions connection/connection_params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConnectionParamsError } from "../client/error.ts";
import { fromFileUrl, isAbsolute } from "../deps.ts";
import { OidType } from "../query/oid.ts";
import { DebugControls } from "../debug.ts";
import { ParseArrayFunction } from "../query/array_parser.ts";

/**
* The connection string must match the following URI structure. All parameters but database and user are optional
Expand Down Expand Up @@ -108,9 +109,16 @@ export type Decoders = {

/**
* A decoder function that takes a string value and returns a parsed value of some type.
* the Oid is also passed to the function for reference
*
* @param value The string value to parse
* @param oid The OID of the column type the value is from
* @param parseArray A helper function that parses SQL array-formatted strings and parses each array value using a transform function.
*/
export type DecoderFunction = (value: string, oid: number) => unknown;
export type DecoderFunction = (
value: string,
oid: number,
parseArray: ParseArrayFunction,
) => unknown;

/**
* Control the behavior for the client instance
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lock": false,
"name": "@bartlomieju/postgres",
"version": "0.19.0",
"version": "0.19.1",
"exports": "./mod.ts"
}
36 changes: 33 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,10 +758,10 @@ available:
You can also provide custom decoders to the client that will be used to decode
the result data. This can be done by setting the `decoders` controls option in
the client configuration. This option is a map object where the keys are the
type names or Oid numbers and the values are the custom decoder functions.
type names or OID numbers and the values are the custom decoder functions.

You can use it with the decode strategy. Custom decoders take precedence over
the strategy and internal parsers.
the strategy and internal decoders.

```ts
{
Expand All @@ -785,7 +785,37 @@ the strategy and internal parsers.
const result = await client.queryObject(
"SELECT ID, NAME, IS_ACTIVE FROM PEOPLE",
);
console.log(result.rows[0]); // {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}}
console.log(result.rows[0]);
// {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}}
}
```

The driver takes care of parsing the related `array` OID types automatically.
For example, if a custom decoder is defined for the `int4` type, it will be
applied when parsing `int4[]` arrays. If needed, you can have separate custom
decoders for the array and non-array types by defining another custom decoders
for the array type itself.

```ts
{
const client = new Client({
database: "some_db",
user: "some_user",
controls: {
decodeStrategy: "string",
decoders: {
// Custom decoder for int4 (OID 23 = int4)
// convert to int and multiply by 100
23: (value: string) => parseInt(value, 10) * 100,
},
},
});

const result = await client.queryObject(
"SELECT ARRAY[ 2, 2, 3, 1 ] AS scores, 8 final_score;",
);
console.log(result.rows[0]);
// { scores: [ 200, 200, 300, 100 ], final_score: 800 }
}
```

Expand Down
10 changes: 10 additions & 0 deletions query/array_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ type AllowedSeparators = "," | ";";
type ArrayResult<T> = Array<T | null | ArrayResult<T>>;
type Transformer<T> = (value: string) => T;

export type ParseArrayFunction = typeof parseArray;

/**
* Parse a string into an array of values using the provided transform function.
*
* @param source The string to parse
* @param transform A function to transform each value in the array
* @param separator The separator used to split the string into values
* @returns
*/
export function parseArray<T>(
source: string,
transform: Transformer<T>,
Expand Down
24 changes: 21 additions & 3 deletions query/decode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Oid, OidTypes, OidValue } from "./oid.ts";
import { Oid, OidType, OidTypes, OidValue } from "./oid.ts";
import { bold, yellow } from "../deps.ts";
import {
decodeBigint,
Expand Down Expand Up @@ -36,6 +36,7 @@ import {
decodeTidArray,
} from "./decoders.ts";
import { ClientControls } from "../connection/connection_params.ts";
import { parseArray } from "./array_parser.ts";

export class Column {
constructor(
Expand Down Expand Up @@ -216,12 +217,29 @@ export function decode(

// check if there is a custom decoder
if (controls?.decoders) {
const oidType = OidTypes[column.typeOid as OidValue];
// check if there is a custom decoder by oid (number) or by type name (string)
const decoderFunc = controls.decoders?.[column.typeOid] ||
controls.decoders?.[OidTypes[column.typeOid as OidValue]];
controls.decoders?.[oidType];

if (decoderFunc) {
return decoderFunc(strValue, column.typeOid);
return decoderFunc(strValue, column.typeOid, parseArray);
} // if no custom decoder is found and the oid is for an array type, check if there is
// a decoder for the base type and use that with the array parser
else if (oidType.includes("_array")) {
const baseOidType = oidType.replace("_array", "") as OidType;
// check if the base type is in the Oid object
if (baseOidType in Oid) {
// check if there is a custom decoder for the base type by oid (number) or by type name (string)
const decoderFunc = controls.decoders?.[Oid[baseOidType]] ||
controls.decoders?.[baseOidType];
if (decoderFunc) {
return parseArray(
strValue,
(value: string) => decoderFunc(value, column.typeOid, parseArray),
);
}
}
}
}

Expand Down
10 changes: 6 additions & 4 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Testing

To run tests, first prepare your configuration file by copying
To run tests, we recommend using Docker. With Docker, there is no need to modify
any configuration, just run the build and test commands.

If running tests on your host, prepare your configuration file by copying
`config.example.json` into `config.json` and updating it appropriately based on
your environment. If you use the Docker based configuration below there's no
need to modify the configuration.
your environment.

## Running the Tests

Expand All @@ -23,7 +25,7 @@ docker-compose run tests
If you have Docker installed then you can run the following to set up a running
container that is compatible with the tests:

```
```sh
docker run --rm --env POSTGRES_USER=test --env POSTGRES_PASSWORD=test \
--env POSTGRES_DB=deno_postgres -p 5432:5432 postgres:12-alpine
```
73 changes: 73 additions & 0 deletions tests/query_client_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,79 @@ Deno.test(
),
);

Deno.test(
"Custom decoders with arrays",
withClient(
async (client) => {
const result = await client.queryObject(
`SELECT
ARRAY[true, false, true] AS _bool_array,
ARRAY['2024-01-01'::date, '2024-01-02'::date, '2024-01-03'::date] AS _date_array,
ARRAY[1.5:: REAL, 2.5::REAL, 3.5::REAL] AS _float_array,
ARRAY[10, 20, 30] AS _int_array,
ARRAY[
'{"key1": "value1", "key2": "value2"}'::jsonb,
'{"key3": "value3", "key4": "value4"}'::jsonb,
'{"key5": "value5", "key6": "value6"}'::jsonb
] AS _jsonb_array,
ARRAY['string1', 'string2', 'string3'] AS _text_array
;`,
);

assertEquals(result.rows, [
{
_bool_array: [
{ boolean: true },
{ boolean: false },
{ boolean: true },
],
_date_array: [
new Date("2024-01-11T00:00:00.000Z"),
new Date("2024-01-12T00:00:00.000Z"),
new Date("2024-01-13T00:00:00.000Z"),
],
_float_array: [15, 25, 35],
_int_array: [110, 120, 130],
_jsonb_array: [
{ key1: "value1", key2: "value2" },
{ key3: "value3", key4: "value4" },
{ key5: "value5", key6: "value6" },
],
_text_array: ["string1_!", "string2_!", "string3_!"],
},
]);
},
{
controls: {
decoders: {
// convert to object
[Oid.bool]: (value: string) => ({ boolean: value === "t" }),
// 1082 = date : convert to date and add 10 days
"1082": (value: string) => {
const d = new Date(value);
return new Date(d.setDate(d.getDate() + 10));
},
// multiply by 20, should not be used!
float4: (value: string) => parseFloat(value) * 20,
// multiply by 10
float4_array: (value: string, _, parseArray) =>
parseArray(value, (v) => parseFloat(v) * 10),
// return 0, should not be used!
[Oid.int4]: () => 0,
// add 100
[Oid.int4_array]: (value: string, _, parseArray) =>
parseArray(value, (v) => parseInt(v, 10) + 100),
// split string and reverse, should not be used!
[Oid.text]: (value: string) => value.split("").reverse(),
// 1009 = text_array : append "_!" to each string
1009: (value: string, _, parseArray) =>
parseArray(value, (v) => `${v}_!`),
},
},
},
),
);

Deno.test(
"Custom decoder precedence",
withClient(
Expand Down
Loading