Skip to content

Commit 91e9f0b

Browse files
authored
feat: support for DECIMAL type (#56)
* feat: support decimal * feat: add examples for DECIMAL data type usage in README * feat: update decimal handling in SenderBuffer classes for protocol v3 support * fix: update write methods to use little-endian format for integers and doubles * feat: enhance decimal handling in SenderBufferV3 and Sender classes to support null and undefined values * fix: address review comments * chore: update subproject commit reference
1 parent 58507a4 commit 91e9f0b

File tree

14 files changed

+685
-31
lines changed

14 files changed

+685
-31
lines changed

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,60 @@ function rndInt(limit: number) {
312312
run().then(console.log).catch(console.error);
313313
```
314314

315+
### Decimal usage example
316+
317+
Since v9.2.0, QuestDB supports the DECIMAL data type.
318+
Decimals can be ingested with ILP protocol v3 using either textual or binary representation.
319+
320+
#### Textual representation
321+
322+
```typescript
323+
import { Sender } from "@questdb/nodejs-client";
324+
325+
async function runDecimals() {
326+
const sender = await Sender.fromConfig(
327+
"tcp::addr=127.0.0.1:9009;protocol_version=3",
328+
);
329+
330+
await sender
331+
.table("fx")
332+
// textual ILP form keeps the literal and its exact scale
333+
.decimalColumnText("mid", "1.234500")
334+
.atNow();
335+
336+
await sender.flush();
337+
await sender.close();
338+
}
339+
340+
runDecimals().catch(console.error);
341+
// Resulting ILP line: fx mid=1.234500d
342+
```
343+
344+
#### Binary representation
345+
346+
It is recommended to use the binary representation for better ingestion performance and reduced payload size (for bigger decimals).
347+
348+
```typescript
349+
import { Sender } from "@questdb/nodejs-client";
350+
351+
async function runDecimals() {
352+
const sender = await Sender.fromConfig(
353+
"tcp::addr=127.0.0.1:9009;protocol_version=3",
354+
);
355+
356+
await sender
357+
.table("fx")
358+
// textual ILP form keeps the literal and its exact scale
359+
.decimalColumn("mid", 123456n, 3) // 123456 * 10^-3 = 123.456
360+
.atNow();
361+
362+
await sender.flush();
363+
await sender.close();
364+
}
365+
366+
runDecimals().catch(console.error);
367+
```
368+
315369
## Community
316370

317371
If you need help, have additional questions or want to provide feedback, you

questdb-client-test

src/buffer/base.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,43 @@ abstract class SenderBufferBase implements SenderBuffer {
522522
}
523523
}
524524
}
525+
526+
/* eslint-disable @typescript-eslint/no-unused-vars */
527+
/**
528+
* Writes a decimal value into the buffer using the text format.
529+
*
530+
* Use it to insert into DECIMAL database columns.
531+
*
532+
* @param {string} name - Column name.
533+
* @param {number} value - Column value, accepts only number/string values.
534+
* @returns {Sender} Returns with a reference to this buffer.
535+
* @throws Error if decimals are not supported by the buffer implementation, or decimal validation fails:
536+
* - string value is not a valid decimal representation
537+
*/
538+
decimalColumnText(name: string, value: string | number): SenderBuffer {
539+
throw new Error("Decimals are not supported in protocol v1/v2");
540+
}
541+
542+
/**
543+
* Writes a decimal value into the buffer using the binary format.
544+
*
545+
* Use it to insert into DECIMAL database columns.
546+
*
547+
* @param {string} name - Column name.
548+
* @param {number} unscaled - The unscaled value of the decimal in two's
549+
* complement representation and big-endian byte order.
550+
* An empty array represents the NULL value.
551+
* @param {number} scale - The scale of the decimal value.
552+
* @returns {Sender} Returns with a reference to this buffer.
553+
*/
554+
decimalColumn(
555+
name: string,
556+
unscaled: Int8Array | bigint,
557+
scale: number,
558+
): SenderBuffer {
559+
throw new Error("Decimals are not supported in protocol v1/v2");
560+
}
561+
/* eslint-enable @typescript-eslint/no-unused-vars */
525562
}
526563

527564
export { SenderBufferBase };

src/buffer/bufferv3.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// @ts-check
2+
import { SenderOptions } from "../options";
3+
import { SenderBuffer } from "./index";
4+
import { bigintToTwosComplementBytes } from "../utils";
5+
import { SenderBufferV2 } from "./bufferv2";
6+
import { validateDecimalText } from "../validation";
7+
8+
// Entity type constants for protocol v3.
9+
const ENTITY_TYPE_DECIMAL: number = 23;
10+
11+
// ASCII code for equals sign used in binary protocol.
12+
const EQUALS_SIGN: number = "=".charCodeAt(0);
13+
14+
/**
15+
* Buffer implementation for protocol version 3.
16+
*
17+
* Provides support for decimals.
18+
*/
19+
class SenderBufferV3 extends SenderBufferV2 {
20+
/**
21+
* Creates a new SenderBufferV3 instance.
22+
*
23+
* @param {SenderOptions} options - Sender configuration object.
24+
*
25+
* See SenderOptions documentation for detailed description of configuration options.
26+
*/
27+
constructor(options: SenderOptions) {
28+
super(options);
29+
}
30+
31+
/**
32+
* Writes a decimal value into the buffer using the text format.
33+
*
34+
* Use it to insert into DECIMAL database columns.
35+
*
36+
* @param {string} name - Column name.
37+
* @param {number} value - Column value, accepts only number/string values.
38+
* @returns {Sender} Returns with a reference to this buffer.
39+
* @throws Error if decimals are not supported by the buffer implementation, or decimal validation fails:
40+
* - string value is not a valid decimal representation
41+
*/
42+
decimalColumnText(name: string, value: string | number): SenderBuffer {
43+
let str = "";
44+
if (typeof value === "string") {
45+
validateDecimalText(value);
46+
str = value;
47+
} else if (typeof value === "number") {
48+
str = value.toString();
49+
} else {
50+
throw new TypeError(`Invalid decimal value type: ${typeof value}`);
51+
}
52+
this.writeColumn(name, str, () => {
53+
this.checkCapacity([str], 1);
54+
this.write(str);
55+
this.write("d");
56+
});
57+
return this;
58+
}
59+
60+
/**
61+
* Writes a decimal value into the buffer using the binary format.
62+
*
63+
* Use it to insert into DECIMAL database columns.
64+
*
65+
* @param {string} name - Column name.
66+
* @param {number} unscaled - The unscaled value of the decimal in two's
67+
* complement representation and big-endian byte order.
68+
* An empty array represents the NULL value.
69+
* @param {number} scale - The scale of the decimal value.
70+
* @returns {Sender} Returns with a reference to this buffer.
71+
* @throws Error if decimals are not supported by the buffer implementation, or decimal validation fails:
72+
* - unscaled value length is not between 0 and 32 bytes
73+
* - scale is not between 0 and 76
74+
* - unscaled value contains invalid bytes
75+
*/
76+
decimalColumn(
77+
name: string,
78+
unscaled: Int8Array | bigint,
79+
scale: number,
80+
): SenderBuffer {
81+
if (scale < 0 || scale > 76) {
82+
throw new RangeError("Scale must be between 0 and 76");
83+
}
84+
let arr: number[];
85+
if (typeof unscaled === "bigint") {
86+
arr = bigintToTwosComplementBytes(unscaled);
87+
} else if (unscaled instanceof Int8Array) {
88+
arr = Array.from(unscaled);
89+
} else {
90+
throw new TypeError(
91+
`Invalid unscaled value type: ${typeof unscaled}, expected Int8Array or bigint`,
92+
);
93+
}
94+
if (arr.length > 32) {
95+
throw new RangeError(
96+
"Unscaled value length must be between 0 and 32 bytes",
97+
);
98+
}
99+
this.writeColumn(name, unscaled, () => {
100+
this.checkCapacity([], 4 + arr.length);
101+
this.writeByte(EQUALS_SIGN);
102+
this.writeByte(ENTITY_TYPE_DECIMAL);
103+
this.writeByte(scale);
104+
this.writeByte(arr.length);
105+
for (let i = 0; i < arr.length; i++) {
106+
let byte = arr[i];
107+
if (byte > 255 || byte < -128) {
108+
throw new RangeError(
109+
`Unscaled value contains invalid byte [index=${i}, value=${byte}]`,
110+
);
111+
}
112+
if (byte > 127) {
113+
byte -= 256;
114+
}
115+
this.writeByte(byte);
116+
}
117+
});
118+
return this;
119+
}
120+
}
121+
122+
export { SenderBufferV3 };

src/buffer/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
PROTOCOL_VERSION_V1,
77
PROTOCOL_VERSION_V2,
88
PROTOCOL_VERSION_AUTO,
9+
PROTOCOL_VERSION_V3,
910
} from "../options";
1011
import { TimestampUnit } from "../utils";
1112
import { SenderBufferV1 } from "./bufferv1";
1213
import { SenderBufferV2 } from "./bufferv2";
14+
import { SenderBufferV3 } from "./bufferv3";
1315

1416
// Default initial buffer size in bytes (64 KB).
1517
const DEFAULT_BUFFER_SIZE = 65536; // 64 KB
@@ -26,6 +28,8 @@ const DEFAULT_MAX_BUFFER_SIZE = 104857600; // 100 MB
2628
*/
2729
function createBuffer(options: SenderOptions): SenderBuffer {
2830
switch (options.protocol_version) {
31+
case PROTOCOL_VERSION_V3:
32+
return new SenderBufferV3(options);
2933
case PROTOCOL_VERSION_V2:
3034
return new SenderBufferV2(options);
3135
case PROTOCOL_VERSION_V1:
@@ -170,6 +174,35 @@ interface SenderBuffer {
170174
unit: TimestampUnit,
171175
): SenderBuffer;
172176

177+
/**
178+
* Writes a decimal value into the buffer using the text format.
179+
*
180+
* Use it to insert into DECIMAL database columns.
181+
*
182+
* @param {string} name - Column name.
183+
* @param {number} value - Column value, accepts only number/string values.
184+
* @returns {Sender} Returns with a reference to this buffer.
185+
*/
186+
decimalColumnText(name: string, value: string | number): SenderBuffer;
187+
188+
/**
189+
* Writes a decimal value into the buffer using the binary format.
190+
*
191+
* Use it to insert into DECIMAL database columns.
192+
*
193+
* @param {string} name - Column name.
194+
* @param {number} unscaled - The unscaled value of the decimal in two's
195+
* complement representation and big-endian byte order.
196+
* An empty array represents the NULL value.
197+
* @param {number} scale - The scale of the decimal value.
198+
* @returns {Sender} Returns with a reference to this buffer.
199+
*/
200+
decimalColumn(
201+
name: string,
202+
unscaled: Int8Array | bigint,
203+
scale: number,
204+
): SenderBuffer;
205+
173206
/**
174207
* Closes the row after writing the designated timestamp into the buffer.
175208
*

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export { TcpTransport } from "./transport/tcp";
1717
export { HttpTransport } from "./transport/http/stdlib";
1818
export { UndiciTransport } from "./transport/http/undici";
1919
export type { Logger } from "./logging";
20+
export { bigintToTwosComplementBytes } from "./utils";

src/options.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const UNSAFE_OFF = "unsafe_off";
2323
const PROTOCOL_VERSION_AUTO = "auto";
2424
const PROTOCOL_VERSION_V1 = "1";
2525
const PROTOCOL_VERSION_V2 = "2";
26+
const PROTOCOL_VERSION_V3 = "3";
2627

2728
const LINE_PROTO_SUPPORT_VERSION = "line.proto.support.versions";
2829

@@ -258,6 +259,8 @@ class SenderOptions {
258259

259260
if (supportedVersions.length === 0) {
260261
options.protocol_version = PROTOCOL_VERSION_V1;
262+
} else if (supportedVersions.includes(PROTOCOL_VERSION_V3)) {
263+
options.protocol_version = PROTOCOL_VERSION_V3;
261264
} else if (supportedVersions.includes(PROTOCOL_VERSION_V2)) {
262265
options.protocol_version = PROTOCOL_VERSION_V2;
263266
} else if (supportedVersions.includes(PROTOCOL_VERSION_V1)) {
@@ -488,10 +491,11 @@ function parseProtocolVersion(options: SenderOptions) {
488491
break;
489492
case PROTOCOL_VERSION_V1:
490493
case PROTOCOL_VERSION_V2:
494+
case PROTOCOL_VERSION_V3:
491495
break;
492496
default:
493497
throw new Error(
494-
`Invalid protocol version: '${protocol_version}', accepted values: 'auto', '1', '2'`,
498+
`Invalid protocol version: '${protocol_version}', accepted values: 'auto', '1', '2', '3'`,
495499
);
496500
}
497501
return;
@@ -628,4 +632,5 @@ export {
628632
PROTOCOL_VERSION_AUTO,
629633
PROTOCOL_VERSION_V1,
630634
PROTOCOL_VERSION_V2,
635+
PROTOCOL_VERSION_V3,
631636
};

src/sender.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,47 @@ class Sender {
342342
return this;
343343
}
344344

345+
/**
346+
* Writes a decimal value into the buffer using the text format.
347+
*
348+
* Use it to insert into DECIMAL database columns.
349+
*
350+
* @param {string} name - Column name.
351+
* @param {number} value - Column value, accepts only number/string values.
352+
* @returns {Sender} Returns with a reference to this buffer.
353+
* @throws Error if decimals are not supported by the buffer implementation, or decimal validation fails:
354+
* - string value is not a valid decimal representation
355+
*/
356+
decimalColumnText(name: string, value: string | number): Sender {
357+
this.buffer.decimalColumnText(name, value);
358+
return this;
359+
}
360+
361+
/**
362+
* Writes a decimal value into the buffer using the binary format.
363+
*
364+
* Use it to insert into DECIMAL database columns.
365+
*
366+
* @param {string} name - Column name.
367+
* @param {number} unscaled - The unscaled value of the decimal in two's
368+
* complement representation and big-endian byte order.
369+
* An empty array represents the NULL value.
370+
* @param {number} scale - The scale of the decimal value.
371+
* @returns {Sender} Returns with a reference to this buffer.
372+
* @throws Error if decimals are not supported by the buffer implementation, or decimal validation fails:
373+
* - unscaled value length is not between 0 and 32 bytes
374+
* - scale is not between 0 and 76
375+
* - unscaled value contains invalid bytes
376+
*/
377+
decimalColumn(
378+
name: string,
379+
unscaled: Int8Array | bigint,
380+
scale: number,
381+
): Sender {
382+
this.buffer.decimalColumn(name, unscaled, scale);
383+
return this;
384+
}
385+
345386
/**
346387
* Closes the row after writing the designated timestamp into the buffer of the sender.
347388
*

0 commit comments

Comments
 (0)