Skip to content

Commit 3d4e619

Browse files
authored
Handle array type custom decoders (#470)
* feat: add logic to handle array types for custom decoders * chore: update docs * chore: fix format * chore: bump version, fix type name * chore: update test readme * chore: format readme
1 parent df6a649 commit 3d4e619

File tree

7 files changed

+154
-13
lines changed

7 files changed

+154
-13
lines changed

connection/connection_params.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ConnectionParamsError } from "../client/error.ts";
33
import { fromFileUrl, isAbsolute } from "../deps.ts";
44
import { OidType } from "../query/oid.ts";
55
import { DebugControls } from "../debug.ts";
6+
import { ParseArrayFunction } from "../query/array_parser.ts";
67

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

109110
/**
110111
* A decoder function that takes a string value and returns a parsed value of some type.
111-
* the Oid is also passed to the function for reference
112+
*
113+
* @param value The string value to parse
114+
* @param oid The OID of the column type the value is from
115+
* @param parseArray A helper function that parses SQL array-formatted strings and parses each array value using a transform function.
112116
*/
113-
export type DecoderFunction = (value: string, oid: number) => unknown;
117+
export type DecoderFunction = (
118+
value: string,
119+
oid: number,
120+
parseArray: ParseArrayFunction,
121+
) => unknown;
114122

115123
/**
116124
* Control the behavior for the client instance

deno.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"lock": false,
33
"name": "@bartlomieju/postgres",
4-
"version": "0.19.0",
4+
"version": "0.19.1",
55
"exports": "./mod.ts"
66
}

docs/README.md

+33-3
Original file line numberDiff line numberDiff line change
@@ -758,10 +758,10 @@ available:
758758
You can also provide custom decoders to the client that will be used to decode
759759
the result data. This can be done by setting the `decoders` controls option in
760760
the client configuration. This option is a map object where the keys are the
761-
type names or Oid numbers and the values are the custom decoder functions.
761+
type names or OID numbers and the values are the custom decoder functions.
762762

763763
You can use it with the decode strategy. Custom decoders take precedence over
764-
the strategy and internal parsers.
764+
the strategy and internal decoders.
765765

766766
```ts
767767
{
@@ -785,7 +785,37 @@ the strategy and internal parsers.
785785
const result = await client.queryObject(
786786
"SELECT ID, NAME, IS_ACTIVE FROM PEOPLE",
787787
);
788-
console.log(result.rows[0]); // {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}}
788+
console.log(result.rows[0]);
789+
// {id: '1', name: 'Javier', is_active: { value: false, type: "boolean"}}
790+
}
791+
```
792+
793+
The driver takes care of parsing the related `array` OID types automatically.
794+
For example, if a custom decoder is defined for the `int4` type, it will be
795+
applied when parsing `int4[]` arrays. If needed, you can have separate custom
796+
decoders for the array and non-array types by defining another custom decoders
797+
for the array type itself.
798+
799+
```ts
800+
{
801+
const client = new Client({
802+
database: "some_db",
803+
user: "some_user",
804+
controls: {
805+
decodeStrategy: "string",
806+
decoders: {
807+
// Custom decoder for int4 (OID 23 = int4)
808+
// convert to int and multiply by 100
809+
23: (value: string) => parseInt(value, 10) * 100,
810+
},
811+
},
812+
});
813+
814+
const result = await client.queryObject(
815+
"SELECT ARRAY[ 2, 2, 3, 1 ] AS scores, 8 final_score;",
816+
);
817+
console.log(result.rows[0]);
818+
// { scores: [ 200, 200, 300, 100 ], final_score: 800 }
789819
}
790820
```
791821

query/array_parser.ts

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ type AllowedSeparators = "," | ";";
66
type ArrayResult<T> = Array<T | null | ArrayResult<T>>;
77
type Transformer<T> = (value: string) => T;
88

9+
export type ParseArrayFunction = typeof parseArray;
10+
11+
/**
12+
* Parse a string into an array of values using the provided transform function.
13+
*
14+
* @param source The string to parse
15+
* @param transform A function to transform each value in the array
16+
* @param separator The separator used to split the string into values
17+
* @returns
18+
*/
919
export function parseArray<T>(
1020
source: string,
1121
transform: Transformer<T>,

query/decode.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Oid, OidTypes, OidValue } from "./oid.ts";
1+
import { Oid, OidType, OidTypes, OidValue } from "./oid.ts";
22
import { bold, yellow } from "../deps.ts";
33
import {
44
decodeBigint,
@@ -36,6 +36,7 @@ import {
3636
decodeTidArray,
3737
} from "./decoders.ts";
3838
import { ClientControls } from "../connection/connection_params.ts";
39+
import { parseArray } from "./array_parser.ts";
3940

4041
export class Column {
4142
constructor(
@@ -216,12 +217,29 @@ export function decode(
216217

217218
// check if there is a custom decoder
218219
if (controls?.decoders) {
220+
const oidType = OidTypes[column.typeOid as OidValue];
219221
// check if there is a custom decoder by oid (number) or by type name (string)
220222
const decoderFunc = controls.decoders?.[column.typeOid] ||
221-
controls.decoders?.[OidTypes[column.typeOid as OidValue]];
223+
controls.decoders?.[oidType];
222224

223225
if (decoderFunc) {
224-
return decoderFunc(strValue, column.typeOid);
226+
return decoderFunc(strValue, column.typeOid, parseArray);
227+
} // if no custom decoder is found and the oid is for an array type, check if there is
228+
// a decoder for the base type and use that with the array parser
229+
else if (oidType.includes("_array")) {
230+
const baseOidType = oidType.replace("_array", "") as OidType;
231+
// check if the base type is in the Oid object
232+
if (baseOidType in Oid) {
233+
// check if there is a custom decoder for the base type by oid (number) or by type name (string)
234+
const decoderFunc = controls.decoders?.[Oid[baseOidType]] ||
235+
controls.decoders?.[baseOidType];
236+
if (decoderFunc) {
237+
return parseArray(
238+
strValue,
239+
(value: string) => decoderFunc(value, column.typeOid, parseArray),
240+
);
241+
}
242+
}
225243
}
226244
}
227245

tests/README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Testing
22

3-
To run tests, first prepare your configuration file by copying
3+
To run tests, we recommend using Docker. With Docker, there is no need to modify
4+
any configuration, just run the build and test commands.
5+
6+
If running tests on your host, prepare your configuration file by copying
47
`config.example.json` into `config.json` and updating it appropriately based on
5-
your environment. If you use the Docker based configuration below there's no
6-
need to modify the configuration.
8+
your environment.
79

810
## Running the Tests
911

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

26-
```
28+
```sh
2729
docker run --rm --env POSTGRES_USER=test --env POSTGRES_PASSWORD=test \
2830
--env POSTGRES_DB=deno_postgres -p 5432:5432 postgres:12-alpine
2931
```

tests/query_client_test.ts

+73
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,79 @@ Deno.test(
241241
),
242242
);
243243

244+
Deno.test(
245+
"Custom decoders with arrays",
246+
withClient(
247+
async (client) => {
248+
const result = await client.queryObject(
249+
`SELECT
250+
ARRAY[true, false, true] AS _bool_array,
251+
ARRAY['2024-01-01'::date, '2024-01-02'::date, '2024-01-03'::date] AS _date_array,
252+
ARRAY[1.5:: REAL, 2.5::REAL, 3.5::REAL] AS _float_array,
253+
ARRAY[10, 20, 30] AS _int_array,
254+
ARRAY[
255+
'{"key1": "value1", "key2": "value2"}'::jsonb,
256+
'{"key3": "value3", "key4": "value4"}'::jsonb,
257+
'{"key5": "value5", "key6": "value6"}'::jsonb
258+
] AS _jsonb_array,
259+
ARRAY['string1', 'string2', 'string3'] AS _text_array
260+
;`,
261+
);
262+
263+
assertEquals(result.rows, [
264+
{
265+
_bool_array: [
266+
{ boolean: true },
267+
{ boolean: false },
268+
{ boolean: true },
269+
],
270+
_date_array: [
271+
new Date("2024-01-11T00:00:00.000Z"),
272+
new Date("2024-01-12T00:00:00.000Z"),
273+
new Date("2024-01-13T00:00:00.000Z"),
274+
],
275+
_float_array: [15, 25, 35],
276+
_int_array: [110, 120, 130],
277+
_jsonb_array: [
278+
{ key1: "value1", key2: "value2" },
279+
{ key3: "value3", key4: "value4" },
280+
{ key5: "value5", key6: "value6" },
281+
],
282+
_text_array: ["string1_!", "string2_!", "string3_!"],
283+
},
284+
]);
285+
},
286+
{
287+
controls: {
288+
decoders: {
289+
// convert to object
290+
[Oid.bool]: (value: string) => ({ boolean: value === "t" }),
291+
// 1082 = date : convert to date and add 10 days
292+
"1082": (value: string) => {
293+
const d = new Date(value);
294+
return new Date(d.setDate(d.getDate() + 10));
295+
},
296+
// multiply by 20, should not be used!
297+
float4: (value: string) => parseFloat(value) * 20,
298+
// multiply by 10
299+
float4_array: (value: string, _, parseArray) =>
300+
parseArray(value, (v) => parseFloat(v) * 10),
301+
// return 0, should not be used!
302+
[Oid.int4]: () => 0,
303+
// add 100
304+
[Oid.int4_array]: (value: string, _, parseArray) =>
305+
parseArray(value, (v) => parseInt(v, 10) + 100),
306+
// split string and reverse, should not be used!
307+
[Oid.text]: (value: string) => value.split("").reverse(),
308+
// 1009 = text_array : append "_!" to each string
309+
1009: (value: string, _, parseArray) =>
310+
parseArray(value, (v) => `${v}_!`),
311+
},
312+
},
313+
},
314+
),
315+
);
316+
244317
Deno.test(
245318
"Custom decoder precedence",
246319
withClient(

0 commit comments

Comments
 (0)