Skip to content

Commit d3d9b8e

Browse files
authored
feat(nodejs): array support (#50)
1 parent 1d43359 commit d3d9b8e

File tree

9 files changed

+737
-2
lines changed

9 files changed

+737
-2
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
branches:
66
- main
77
pull_request:
8+
schedule:
9+
- cron: '15 2,10,18 * * *'
810

911
jobs:
1012
test:

src/buffer/base.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const DEFAULT_MAX_NAME_LENGTH = 127;
2626
abstract class SenderBufferBase implements SenderBuffer {
2727
private bufferSize: number;
2828
private readonly maxBufferSize: number;
29-
private buffer: Buffer<ArrayBuffer>;
30-
private position: number;
29+
protected buffer: Buffer<ArrayBuffer>;
30+
protected position: number;
3131
private endOfLastRow: number;
3232

3333
private hasTable: boolean;
@@ -232,6 +232,15 @@ abstract class SenderBufferBase implements SenderBuffer {
232232
*/
233233
abstract floatColumn(name: string, value: number): SenderBuffer;
234234

235+
/**
236+
* Writes an array column with its values into the buffer.
237+
*
238+
* @param {string} name - Column name.
239+
* @param {unknown[]} value - Column value, accepts only arrays.
240+
* @return {Sender} Returns with a reference to this sender.
241+
*/
242+
abstract arrayColumn(name: string, value: unknown[]): SenderBuffer;
243+
235244
/**
236245
* Writes a 64-bit signed integer into the buffer. <br>
237246
* Use it to insert into LONG, INT, SHORT and BYTE columns.
@@ -386,6 +395,10 @@ abstract class SenderBufferBase implements SenderBuffer {
386395
this.position = this.buffer.writeInt8(data, this.position);
387396
}
388397

398+
protected writeInt(data: number) {
399+
this.position = this.buffer.writeInt32LE(data, this.position);
400+
}
401+
389402
protected writeDouble(data: number) {
390403
this.position = this.buffer.writeDoubleLE(data, this.position);
391404
}

src/buffer/bufferv1.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class SenderBufferV1 extends SenderBufferBase {
3333
);
3434
return this;
3535
}
36+
37+
arrayColumn(): SenderBuffer {
38+
throw new Error("Arrays are not supported in protocol v1");
39+
}
3640
}
3741

3842
export { SenderBufferV1 };

src/buffer/bufferv2.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
import { SenderOptions } from "../options";
33
import { SenderBuffer } from "./index";
44
import { SenderBufferBase } from "./base";
5+
import { ArrayPrimitive, getDimensions, validateArray } from "../utils";
56

7+
const COLUMN_TYPE_DOUBLE: number = 10;
8+
const COLUMN_TYPE_NULL: number = 33;
9+
10+
const ENTITY_TYPE_ARRAY: number = 14;
611
const ENTITY_TYPE_DOUBLE: number = 16;
12+
713
const EQUALS_SIGN: number = "=".charCodeAt(0);
814

915
/**
@@ -37,6 +43,91 @@ class SenderBufferV2 extends SenderBufferBase {
3743
);
3844
return this;
3945
}
46+
47+
arrayColumn(name: string, value: unknown[]): SenderBuffer {
48+
const dimensions = getDimensions(value);
49+
const type = validateArray(value, dimensions);
50+
// only number arrays and NULL supported for now
51+
if (type !== "number" && type !== null) {
52+
throw new Error(`Unsupported array type [type=${type}]`);
53+
}
54+
55+
this.writeColumn(name, value, () => {
56+
this.checkCapacity([], 3);
57+
this.writeByte(EQUALS_SIGN);
58+
this.writeByte(ENTITY_TYPE_ARRAY);
59+
60+
if (!value) {
61+
this.writeByte(COLUMN_TYPE_NULL);
62+
} else {
63+
this.writeByte(COLUMN_TYPE_DOUBLE);
64+
this.writeArray(value, dimensions, type);
65+
}
66+
});
67+
return this;
68+
}
69+
70+
private writeArray(
71+
arr: unknown[],
72+
dimensions: number[],
73+
type: ArrayPrimitive,
74+
) {
75+
this.checkCapacity([], 1 + dimensions.length * 4);
76+
this.writeByte(dimensions.length);
77+
for (let i = 0; i < dimensions.length; i++) {
78+
this.writeInt(dimensions[i]);
79+
}
80+
81+
this.checkCapacity([], SenderBufferV2.arraySize(dimensions, type));
82+
this.writeArrayValues(arr, dimensions);
83+
}
84+
85+
private writeArrayValues(arr: unknown[], dimensions: number[]) {
86+
if (Array.isArray(arr[0])) {
87+
for (let i = 0; i < arr.length; i++) {
88+
this.writeArrayValues(arr[i] as unknown[], dimensions);
89+
}
90+
} else {
91+
const type = arr[0] !== undefined ? typeof arr[0] : null;
92+
switch (type) {
93+
case "number":
94+
for (let i = 0; i < arr.length; i++) {
95+
this.position = this.buffer.writeDoubleLE(
96+
arr[i] as number,
97+
this.position,
98+
);
99+
}
100+
break;
101+
case null:
102+
// empty array
103+
break;
104+
default:
105+
throw new Error(`Unsupported array type [type=${type}]`);
106+
}
107+
}
108+
}
109+
110+
private static arraySize(dimensions: number[], type: ArrayPrimitive): number {
111+
let numOfElements = 1;
112+
for (let i = 0; i < dimensions.length; i++) {
113+
numOfElements *= dimensions[i];
114+
}
115+
116+
switch (type) {
117+
case "number":
118+
return numOfElements * 8;
119+
case "boolean":
120+
return numOfElements;
121+
case "string":
122+
// in case of string[] capacity check is done separately for each array element
123+
return 0;
124+
case null:
125+
// empty array
126+
return 0;
127+
default:
128+
throw new Error(`Unsupported array type [type=${type}]`);
129+
}
130+
}
40131
}
41132

42133
export { SenderBufferV2 };

src/buffer/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ interface SenderBuffer {
104104
*/
105105
floatColumn(name: string, value: number): SenderBuffer;
106106

107+
arrayColumn(name: string, value: unknown[]): SenderBuffer;
108+
107109
/**
108110
* Writes an integer column with its value into the buffer.
109111
*

src/sender.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ class Sender {
241241
return this;
242242
}
243243

244+
arrayColumn(name: string, value: unknown[]): Sender {
245+
this.buffer.arrayColumn(name, value);
246+
return this;
247+
}
248+
244249
/**
245250
* Writes an integer column with its value into the buffer of the sender.
246251
*

src/utils.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Agent } from "undici";
22

3+
type ArrayPrimitive = "number" | "boolean" | "string" | null;
4+
35
type TimestampUnit = "ns" | "us" | "ms";
46

57
function isBoolean(value: unknown): value is boolean {
@@ -38,6 +40,72 @@ function timestampToNanos(timestamp: bigint, unit: TimestampUnit) {
3840
}
3941
}
4042

43+
function getDimensions(data: unknown) {
44+
const dimensions: number[] = [];
45+
while (Array.isArray(data)) {
46+
dimensions.push(data.length);
47+
data = data[0];
48+
}
49+
return dimensions;
50+
}
51+
52+
function validateArray(data: unknown[], dimensions: number[]): ArrayPrimitive {
53+
if (data === null || data === undefined) {
54+
return null;
55+
}
56+
if (!Array.isArray(data)) {
57+
throw new Error(
58+
`The value must be an array [value=${JSON.stringify(data)}, type=${typeof data}]`,
59+
);
60+
}
61+
62+
let expectedType: ArrayPrimitive = null;
63+
64+
function checkArray(
65+
array: unknown[],
66+
depth: number = 0,
67+
path: string = "",
68+
): void {
69+
const expectedLength = dimensions[depth];
70+
if (array.length !== expectedLength) {
71+
throw new Error(
72+
`Lengths of sub-arrays do not match [expected=${expectedLength}, actual=${array.length}, dimensions=[${dimensions}], path=${path}]`,
73+
);
74+
}
75+
76+
if (depth < dimensions.length - 1) {
77+
// intermediate level, expecting arrays
78+
for (let i = 0; i < array.length; i++) {
79+
if (!Array.isArray(array[i])) {
80+
throw new Error(
81+
`Mixed types found [expected=array, current=${typeof array[i]}, path=${path}[${i}]]`,
82+
);
83+
}
84+
checkArray(array[i] as unknown[], depth + 1, `${path}[${i}]`);
85+
}
86+
} else {
87+
// leaf level, expecting primitives
88+
if (expectedType === null && array[0] !== undefined) {
89+
expectedType = typeof array[0] as ArrayPrimitive;
90+
}
91+
92+
for (let i = 0; i < array.length; i++) {
93+
const currentType = typeof array[i] as ArrayPrimitive;
94+
if (currentType !== expectedType) {
95+
throw new Error(
96+
expectedType !== null
97+
? `Mixed types found [expected=${expectedType}, current=${currentType}, path=${path}[${i}]]`
98+
: `Unsupported array type [type=${currentType}]`,
99+
);
100+
}
101+
}
102+
}
103+
}
104+
105+
checkArray(data);
106+
return expectedType;
107+
}
108+
41109
/**
42110
* Fetches JSON data from a URL.
43111
* @template T - The expected type of the JSON response
@@ -83,4 +151,7 @@ export {
83151
timestampToNanos,
84152
TimestampUnit,
85153
fetchJson,
154+
getDimensions,
155+
validateArray,
156+
ArrayPrimitive,
86157
};

0 commit comments

Comments
 (0)