Skip to content

Commit e25858d

Browse files
authored
Merge pull request #90 from malthe/bigint-improvements
Add "bigints" option to change bigint behavior (allow the use of 'number' instead)
2 parents 49b48de + 8322b89 commit e25858d

File tree

8 files changed

+85
-29
lines changed

8 files changed

+85
-29
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
In next release ...
22

3+
- Both the `Query` object and client configuration now provide an optional `bigints`
4+
setting which decides whether to use bigints or the number type for the INT8
5+
data type. The setting is enabled by default.
6+
7+
- Add support for the INT2 and INT8 array types.
8+
39
- The `Connect` and `End` events have been removed, in addition to the `Parameter`
410
event; the `connect` method now returns an object with information about the
511
established connection, namely whether the connection is encrypted and the

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ accessed via the iterator or record interface (see below).
126126

127127
Query parameters use the format `$1`, `$2` etc.
128128

129-
When a specific data type is not inferrable from the query, PostgreSQL
129+
When a specific data type can't be inferred from the query, PostgreSQL
130130
uses `DataType.Text` as the default data type (which is mapped to the
131131
string type in TypeScript). An explicit type can be provided in two
132132
different ways:
@@ -143,9 +143,19 @@ different ways:
143143
```
144144

145145
Note that the `number` type in TypeScript has a maximum safe integer
146-
value which lies between and `DataType.Int8` – given by
147-
`Number.MAX_SAFE_INTEGER`. To use `DataType.Int8` the `bigint` type
148-
should be used.
146+
value which is 2⁵³ – 1 (also given in the `Number.MAX_SAFE_INTEGER` constant),
147+
a value which lies between `DataType.Int4` and `DataType.Int8`. For numbers
148+
which can take on a value that's outside the safe range, use `DataType.Int8`
149+
(which translates to a `bigint` in TypeScript.)
150+
151+
There's an optional setting `bigints` which can be configured on the client and/or
152+
specified for each query. It defaults to _true_, but can be set to _false_ in which
153+
case `number` is always used instead of `bigint` for `DataType.Int8` (throwing an
154+
error if a query returns a value outside of the safe integer range.)
155+
156+
Using a [check constraint](https://www.postgresql.org/docs/current/ddl-constraints.html)
157+
is recommended to ensure that values fit into the safe
158+
integer range, e.g. `CHECK (id < POWER(2, 53) - 1)`.
149159

150160
### Iterator interface
151161

src/client.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export interface Configuration extends Partial<ClientConnectionDefaults & Client
7575
port?: number,
7676
password?: string,
7777
types?: Map<DataType, ValueTypeReader>,
78+
bigints?: boolean,
7879
keepAlive?: boolean,
7980
preparedStatementPrefix?: string,
8081
connectionTimeout?: number,
@@ -117,6 +118,7 @@ type CloseHandler = () => void;
117118
interface RowDataHandler {
118119
callback: DataHandler,
119120
streams: Record<string, Writable>,
121+
bigints: boolean,
120122
}
121123

122124
type DescriptionHandler = (description: RowDescription) => void;
@@ -562,6 +564,7 @@ export class Client {
562564
handler: {
563565
callback: result.dataHandler,
564566
streams: streams || {},
567+
bigints: query.bigints ?? this.config.bigints ?? true,
565568
},
566569
description: description,
567570
};
@@ -607,7 +610,7 @@ export class Client {
607610

608611
const format = query?.format;
609612
const types = query?.types;
610-
const streams =query?.streams;
613+
const streams = query?.streams;
611614
const portal = query?.portal || '';
612615
const result = makeResult<T>(query?.transform);
613616

@@ -618,6 +621,7 @@ export class Client {
618621
const dataHandler: RowDataHandler = {
619622
callback: result.dataHandler,
620623
streams: streams || {},
624+
bigints: query.bigints ?? this.config.bigints ?? true,
621625
};
622626

623627
if (values && values.length) {
@@ -815,7 +819,6 @@ export class Client {
815819
}
816820

817821
private handle(buffer: Buffer, offset: number, size: number): number {
818-
const types = this.config.types || null;
819822
let read = 0;
820823

821824
while (size >= this.expect + read) {
@@ -825,6 +828,7 @@ export class Client {
825828
// Fast path: retrieve data rows.
826829
if (mtype === Message.RowData) {
827830
const info = this.activeDataHandlerInfo;
831+
828832
if (!info) {
829833
throw new Error('No active data handler');
830834
}
@@ -835,8 +839,9 @@ export class Client {
835839

836840
const {
837841
handler: {
838-
streams,
839842
callback,
843+
streams,
844+
bigints,
840845
},
841846
description: {
842847
columns,
@@ -846,16 +851,18 @@ export class Client {
846851

847852
let row = this.activeRow;
848853

854+
const types = this.config.types;
855+
const encoding = this.encoding;
856+
849857
const hasStreams = Object.keys(streams).length > 0;
850858
const mappedStreams = hasStreams ? names.map(
851-
name => streams[name] || null
852-
) : null;
859+
name => streams[name]
860+
) : undefined;
853861

854862
while (true) {
855863
mtype = buffer.readInt8(frame);
856864
if (mtype !== Message.RowData) break;
857865

858-
859866
const bytes = buffer.readInt32BE(frame + 1) + 1;
860867
const start = frame + 5;
861868

@@ -875,7 +882,8 @@ export class Client {
875882
const end = reader.readRowData(
876883
row,
877884
columns,
878-
this.encoding,
885+
encoding,
886+
bigints,
879887
types,
880888
mappedStreams
881889
);

src/protocol.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,9 @@ export function readRowData(
285285
row: Array<any>,
286286
columnSpecification: Uint32Array,
287287
encoding: BufferEncoding,
288-
types: ReadonlyMap<DataType, ValueTypeReader> | null,
289-
streams: ReadonlyArray<Writable | null> | null,
288+
bigints: boolean,
289+
types?: ReadonlyMap<DataType, ValueTypeReader>,
290+
streams?: ReadonlyArray<Writable | null>,
290291
): number {
291292
const columns = row.length;
292293
const bufferLength = buffer.length;
@@ -319,9 +320,9 @@ export function readRowData(
319320
const spec = columnSpecification[j];
320321
let skip = false;
321322

322-
if (streams !== null && spec === DataType.Bytea) {
323+
if (streams && spec === DataType.Bytea) {
323324
const stream = streams[j];
324-
if (stream !== null) {
325+
if (stream) {
325326
const slice = buffer.slice(start, end);
326327
const alloc = Buffer.allocUnsafe(slice.length);
327328
slice.copy(alloc, 0, 0, slice.length);
@@ -350,7 +351,7 @@ export function readRowData(
350351
const isReader = (spec & readerMask) !== 0;
351352

352353
if (isReader) {
353-
const reader = (types) ? types.get(dataType) : null;
354+
const reader = types?.get(dataType);
354355
if (reader) {
355356
value = reader(
356357
buffer,
@@ -404,8 +405,14 @@ export function readRowData(
404405
case DataType.Int4:
405406
case DataType.Oid:
406407
return buffer.readInt32BE(start);
407-
case DataType.Int8:
408-
return buffer.readBigInt64BE(start);
408+
case DataType.Int8: {
409+
const value = buffer.readBigInt64BE(start);
410+
if (bigints) return value;
411+
if (value > Number.MAX_SAFE_INTEGER) {
412+
throw new Error("INT8 value too big for 'number' type");
413+
}
414+
return Number(value);
415+
}
409416
case DataType.Float4:
410417
return buffer.readFloatBE(start);
411418
case DataType.Float8:
@@ -631,14 +638,16 @@ export class Reader {
631638
row: any[],
632639
columnSpecification: Uint32Array,
633640
encoding: BufferEncoding,
634-
types: ReadonlyMap<DataType, ValueTypeReader> | null,
635-
streams: ReadonlyArray<Writable | null> | null,
641+
bigints: boolean,
642+
types?: ReadonlyMap<DataType, ValueTypeReader>,
643+
streams?: ReadonlyArray<Writable | null>,
636644
) {
637645
return readRowData(
638646
this.buffer.slice(this.start, this.end),
639647
row,
640648
columnSpecification,
641649
encoding,
650+
bigints,
642651
types,
643652
streams
644653
);

src/query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface QueryOptions {
1717
readonly streams: Record<string, Writable>;
1818
/** Allows the transformation of column names as returned by the database. */
1919
readonly transform: (name: string) => string;
20+
/** Use bigint for the INT8 (64-bit integer) data type. */
21+
readonly bigints: boolean;
2022
}
2123

2224
/**

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ export enum DataType {
3939
Inet = 869,
4040
ArrayBytea = 1001,
4141
ArrayChar = 1002,
42+
ArrayInt2 = 1005,
4243
ArrayInt4 = 1007,
4344
ArrayRegprocedure = 1008,
4445
ArrayText = 1009,
4546
ArrayBpchar = 1014,
4647
ArrayVarchar = 1015,
48+
ArrayInt8 = 1016,
4749
ArrayFloat4 = 1021,
4850
ArrayFloat8 = 1022,
4951
Aclitem = 1033,
@@ -108,7 +110,9 @@ export const arrayDataTypeMapping: ReadonlyMap<DataType, DataType> = new Map([
108110
[DataType.ArrayDate, DataType.Date],
109111
[DataType.ArrayFloat4, DataType.Float4],
110112
[DataType.ArrayFloat8, DataType.Float8],
113+
[DataType.ArrayInt2, DataType.Int2],
111114
[DataType.ArrayInt4, DataType.Int4],
115+
[DataType.ArrayInt8, DataType.Int8],
112116
[DataType.ArrayJson, DataType.Json],
113117
[DataType.ArrayJsonb, DataType.Jsonb],
114118
[DataType.ArrayText, DataType.Text],

test/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export function testWithClient(name: string, fn: Test, timeout?: number) {
2424
}
2525
if (!client.closed) {
2626
await client.end();
27-
if (!client.closed) throw new Error("Expected client to be closed");
2827
}
2928
}
29+
if (!client.closed) throw new Error("Expected client to be closed");
3030
}, timeout);
3131
}

test/types.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,19 @@ function testType<T>(
2828
dataType: DataType,
2929
expression: string,
3030
expected: T,
31-
excludeTextMode = false) {
31+
excludeTextMode = false,
32+
bigints?: boolean) {
3233
const testParam = (format: DataFormat) => {
3334
testWithClient('Param', async (client) => {
3435
expect.assertions(3);
35-
const query = expected !== null
36-
? getComparisonQueryFor(dataType, expression)
36+
const text = expected !== null
37+
? getComparisonQueryFor(dataType, expression) + ' where $1 is not null'
3738
: 'select $1 is null';
3839
await client.query({
39-
text: expected !== null ? query + ' where $1 is not null' : query,
40+
text,
4041
types: [dataType],
41-
format
42+
format,
43+
bigints,
4244
}, [expected])
4345
.then(
4446
(result) => {
@@ -53,13 +55,13 @@ function testType<T>(
5355
const testValue = (format: DataFormat) => {
5456
testWithClient('Value', async (client) => {
5557
expect.assertions(3);
56-
const query = 'select ' + expression;
57-
await client.query({text: query, format}, []).then(
58+
const text = 'select ' + expression;
59+
await client.query({text, format, bigints}, []).then(
5860
(result) => {
5961
const rows = result.rows;
6062
expect(rows.length).toEqual(1);
6163
expect(rows[0].length).toEqual(1);
62-
expect(rows[0][0]).toEqual(expected)
64+
expect(rows[0][0]).toEqual(expected);
6365
});
6466
})
6567
};
@@ -101,6 +103,7 @@ describe('Types', () => {
101103
testType<number>(DataType.Int2, '1::int2', 1);
102104
testType<number>(DataType.Int4, '1::int4', 1);
103105
testType<bigint>(DataType.Int8, '1::int8', BigInt(1));
106+
testType<number>(DataType.Int8, '1::int8', 1, undefined, false);
104107
testType<number>(DataType.Float4, '1::float4', 1.0);
105108
testType<number>(DataType.Float8, '1::float8', 1.0);
106109
testType<number>(DataType.Oid, '1::oid', 1);
@@ -189,6 +192,10 @@ describe('Types', () => {
189192
'ARRAY[\'123e4567-e89b-12d3-a456-426655440000\'::uuid]',
190193
['123e4567-e89b-12d3-a456-426655440000']
191194
);
195+
testType<number[]>(
196+
DataType.ArrayInt2,
197+
'\'{1,2,3}\'::int2[3]',
198+
[1, 2, 3]);
192199
testType<number[]>(
193200
DataType.ArrayInt4,
194201
'\'{1,2,3}\'::int4[3]',
@@ -201,6 +208,16 @@ describe('Types', () => {
201208
DataType.ArrayInt4,
202209
'\'{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}\'::int4[]',
203210
[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]);
211+
testType<bigint[]>(
212+
DataType.ArrayInt8,
213+
'\'{1,2,3}\'::int8[3]',
214+
[BigInt(1), BigInt(2), BigInt(3)]);
215+
testType<number[]>(
216+
DataType.ArrayInt8,
217+
'\'{1,2,3}\'::int8[3]',
218+
[1, 2, 3],
219+
undefined,
220+
false);
204221
testType<number[]>(
205222
DataType.ArrayFloat4,
206223
'\'{1.0, 2.0, 3.0}\'::float4[3]',

0 commit comments

Comments
 (0)