Skip to content

Commit e70df0d

Browse files
authored
fix(client): parse Presto JSON response with custom reviver and BigInts (#25)
1 parent c114b01 commit e70df0d

File tree

6 files changed

+129
-7
lines changed

6 files changed

+129
-7
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18.17.1
1+
22.12.0

apps/nest-server/src/app/app.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,14 @@ export class AppService {
4848
const results = await client.query(
4949
`select returnflag, linestatus, sum(quantity) as sum_qty, sum(extendedprice) as sum_base_price, sum(extendedprice * (1 - discount)) as sum_disc_price, sum(extendedprice * (1 - discount) * (1 + tax)) as sum_charge, avg(quantity) as avg_qty, avg(extendedprice) as avg_price, avg(discount) as avg_disc, count(*) as count_order from lineitem where shipdate <= date '1998-12-01' group by returnflag, linestatus order by returnflag, linestatus`,
5050
)
51-
return { columns: results.columns, rows: results.data }
51+
return {
52+
columns: results.columns,
53+
rows: JSON.stringify(results.data, (key, value) => {
54+
if (typeof value != 'bigint') return value
55+
56+
return value.toString()
57+
}),
58+
}
5259
} catch (error) {
5360
return (error as PrestoError).message
5461
}

presto-client/README.md

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,18 @@ try {
9797

9898
### Additional notes
9999

100-
Additional notes
100+
Additional notes on the `query` method:
101101

102-
- The `query` method is asynchronous and will return a promise that resolves to a PrestoQuery object.
103-
- The `query` method will automatically retry the query if it fails due to a transient error.
104-
- The `query` method will cancel the query if the client is destroyed.
102+
- It's asynchronous and will return a promise that resolves to a PrestoQuery object.
103+
- It will automatically retry the query if it fails due to a transient error.
104+
- It will cancel the query if the client is destroyed.
105+
- \*It parses big numbers with the BigInt JavaScript primitive. If your Presto response includes a number bigger than `Number.MAX_SAFE_INTEGER`, it will be parsed into a bigint, so you may need to consider that when handling the response, or serializing it.
106+
107+
\* Only if the current JavaScript environment supports the reviver with context in the JSON.parse callback. Check compatibility here:
108+
109+
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#browser_compatibility
110+
111+
Otherwise, bigger numbers will lose precision due to the default JavaScript JSON parsing.
105112

106113
## Get Query metadata information
107114

@@ -240,3 +247,38 @@ const client = new PrestoClient({
240247
user: 'root',
241248
})
242249
```
250+
251+
## Troubleshooting
252+
253+
### Do not know how to serialize a BigInt
254+
255+
Example error message:
256+
257+
```
258+
Do not know how to serialize a BigInt
259+
TypeError: Do not know how to serialize a BigInt
260+
at JSON.stringify (<anonymous>)
261+
```
262+
263+
Make sure to write a custom replacer and handle the serialization of BigInts if your Presto query returns a number bigger than `Number.MAX_SAFE_INTEGER`.
264+
Example JSON.stringify replacer:
265+
266+
```javascript
267+
const results = await client.query(`SELECT 1234567890123456623`)
268+
return {
269+
columns: results.columns,
270+
rows: JSON.stringify(results.data, (key, value) => {
271+
if (typeof value !== 'bigint') return value
272+
273+
return value.toString()
274+
}),
275+
}
276+
```
277+
278+
### Numbers lose precision
279+
280+
Known issue:
281+
If working with numbers bigger than `Number.MAX_SAFE_INTEGER`, and your environment does not support the `JSON.parse` with the context in the reviver (Node.js > 21.0.0, and certain browser versions), the default JSON.parse will make the number lose precision.
282+
Check compatibility here:
283+
284+
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#browser_compatibility

presto-client/src/client.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
QueryInfo,
99
Table,
1010
} from './types'
11+
import { parseWithBigInts } from './utils'
1112

1213
export class PrestoClient {
1314
private baseUrl: string
@@ -270,7 +271,7 @@ export class PrestoClient {
270271
throw new Error(`Query failed: ${JSON.stringify(await response.text())}`)
271272
}
272273

273-
const prestoResponse = (await response.json()) as PrestoResponse
274+
const prestoResponse = (await this.prestoConversionToJSON({ response })) as PrestoResponse
274275
if (!prestoResponse) {
275276
throw new Error(`Query failed with an empty response from the server.`)
276277
}
@@ -334,6 +335,13 @@ export class PrestoClient {
334335
method,
335336
})
336337
}
338+
339+
private async prestoConversionToJSON({ response }: { response: Response }): Promise<unknown> {
340+
const text = await response.text()
341+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
342+
// @ts-ignore JSON.parse with a 3 argument reviver is a stage 3 proposal with some support, allow it here.
343+
return JSON.parse(text, parseWithBigInts)
344+
}
337345
}
338346

339347
export default PrestoClient

presto-client/src/utils.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { parseWithBigInts } from './utils'
2+
3+
describe('parse big ints', () => {
4+
it('can be plugged into JSON.parse', () => {
5+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6+
// @ts-ignore
7+
const primitiveNumber = JSON.parse('123', parseWithBigInts)
8+
expect(primitiveNumber).toBe(123)
9+
10+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
11+
// @ts-ignore
12+
const parsedObject = JSON.parse('{"key": "value"}', parseWithBigInts)
13+
expect(parsedObject).toHaveProperty('key')
14+
expect(parsedObject.key).toBe('value')
15+
})
16+
17+
it('parses when the JSON.parse reviver context is available', () => {
18+
const parsedValue = parseWithBigInts('key', Number.MAX_SAFE_INTEGER, {
19+
source: Number.MIN_SAFE_INTEGER.toString(),
20+
})
21+
expect(parsedValue).toBe(Number.MAX_SAFE_INTEGER)
22+
})
23+
24+
it('parses when the JSON.parse reviver context is not available', () => {
25+
const parsedValue = parseWithBigInts('key', Number.MAX_SAFE_INTEGER, undefined)
26+
expect(parsedValue).toBe(Number.MAX_SAFE_INTEGER)
27+
})
28+
29+
it('parses big integers when JSON.parse reviver context is available', () => {
30+
const largeNumberAsBigInt = BigInt(Number.MAX_SAFE_INTEGER) + 2n
31+
const largeNumberAsNumber = Number.MAX_SAFE_INTEGER + 2
32+
const parsedValue = parseWithBigInts('key', largeNumberAsNumber, {
33+
source: largeNumberAsBigInt.toString(),
34+
})
35+
expect(typeof parsedValue).toEqual('bigint')
36+
expect(parsedValue).toEqual(largeNumberAsBigInt)
37+
})
38+
})

presto-client/src/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Parses a JSON including bigger numbers into BigInts
3+
* This function checks if JSON.parse reviver callback has a context parameter
4+
* and falls back onto the default parsing if not.
5+
* See also:
6+
* - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#browser_compatibility
7+
* - https://github.com/tc39/proposal-json-parse-with-source
8+
* @param _ Key
9+
* @param value Parsed value
10+
* @param context Context with source text
11+
* @returns Parsed object with BigInts where required
12+
*/
13+
export function parseWithBigInts(_: string, value: unknown, context: { source: string } | undefined) {
14+
if (!context?.source) return value // Context source is not available, fallback to default parse
15+
16+
// Ignore non-numbers
17+
if (typeof value != 'number') return value
18+
19+
// If not an integer, use the value
20+
// TODO: Check if Presto can return floats that could also lose precision
21+
if (!Number.isInteger(value)) return value
22+
23+
// If number is a safe integer, we can use it
24+
if (Number.isSafeInteger(value)) return value
25+
26+
return BigInt(context.source)
27+
}

0 commit comments

Comments
 (0)