diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dead93..7c87581 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,8 @@ on: branches: - main pull_request: + schedule: + - cron: '15 2,10,18 * * *' jobs: test: diff --git a/src/buffer/base.ts b/src/buffer/base.ts index 27363d8..14e514d 100644 --- a/src/buffer/base.ts +++ b/src/buffer/base.ts @@ -26,8 +26,8 @@ const DEFAULT_MAX_NAME_LENGTH = 127; abstract class SenderBufferBase implements SenderBuffer { private bufferSize: number; private readonly maxBufferSize: number; - private buffer: Buffer; - private position: number; + protected buffer: Buffer; + protected position: number; private endOfLastRow: number; private hasTable: boolean; @@ -232,6 +232,15 @@ abstract class SenderBufferBase implements SenderBuffer { */ abstract floatColumn(name: string, value: number): SenderBuffer; + /** + * Writes an array column with its values into the buffer. + * + * @param {string} name - Column name. + * @param {unknown[]} value - Column value, accepts only arrays. + * @return {Sender} Returns with a reference to this sender. + */ + abstract arrayColumn(name: string, value: unknown[]): SenderBuffer; + /** * Writes a 64-bit signed integer into the buffer.
* Use it to insert into LONG, INT, SHORT and BYTE columns. @@ -386,6 +395,10 @@ abstract class SenderBufferBase implements SenderBuffer { this.position = this.buffer.writeInt8(data, this.position); } + protected writeInt(data: number) { + this.position = this.buffer.writeInt32LE(data, this.position); + } + protected writeDouble(data: number) { this.position = this.buffer.writeDoubleLE(data, this.position); } diff --git a/src/buffer/bufferv1.ts b/src/buffer/bufferv1.ts index 605837f..239d704 100644 --- a/src/buffer/bufferv1.ts +++ b/src/buffer/bufferv1.ts @@ -33,6 +33,10 @@ class SenderBufferV1 extends SenderBufferBase { ); return this; } + + arrayColumn(): SenderBuffer { + throw new Error("Arrays are not supported in protocol v1"); + } } export { SenderBufferV1 }; diff --git a/src/buffer/bufferv2.ts b/src/buffer/bufferv2.ts index a90fa58..2ad167e 100644 --- a/src/buffer/bufferv2.ts +++ b/src/buffer/bufferv2.ts @@ -2,8 +2,14 @@ import { SenderOptions } from "../options"; import { SenderBuffer } from "./index"; import { SenderBufferBase } from "./base"; +import { ArrayPrimitive, getDimensions, validateArray } from "../utils"; +const COLUMN_TYPE_DOUBLE: number = 10; +const COLUMN_TYPE_NULL: number = 33; + +const ENTITY_TYPE_ARRAY: number = 14; const ENTITY_TYPE_DOUBLE: number = 16; + const EQUALS_SIGN: number = "=".charCodeAt(0); /** @@ -37,6 +43,91 @@ class SenderBufferV2 extends SenderBufferBase { ); return this; } + + arrayColumn(name: string, value: unknown[]): SenderBuffer { + const dimensions = getDimensions(value); + const type = validateArray(value, dimensions); + // only number arrays and NULL supported for now + if (type !== "number" && type !== null) { + throw new Error(`Unsupported array type [type=${type}]`); + } + + this.writeColumn(name, value, () => { + this.checkCapacity([], 3); + this.writeByte(EQUALS_SIGN); + this.writeByte(ENTITY_TYPE_ARRAY); + + if (!value) { + this.writeByte(COLUMN_TYPE_NULL); + } else { + this.writeByte(COLUMN_TYPE_DOUBLE); + this.writeArray(value, dimensions, type); + } + }); + return this; + } + + private writeArray( + arr: unknown[], + dimensions: number[], + type: ArrayPrimitive, + ) { + this.checkCapacity([], 1 + dimensions.length * 4); + this.writeByte(dimensions.length); + for (let i = 0; i < dimensions.length; i++) { + this.writeInt(dimensions[i]); + } + + this.checkCapacity([], SenderBufferV2.arraySize(dimensions, type)); + this.writeArrayValues(arr, dimensions); + } + + private writeArrayValues(arr: unknown[], dimensions: number[]) { + if (Array.isArray(arr[0])) { + for (let i = 0; i < arr.length; i++) { + this.writeArrayValues(arr[i] as unknown[], dimensions); + } + } else { + const type = arr[0] !== undefined ? typeof arr[0] : null; + switch (type) { + case "number": + for (let i = 0; i < arr.length; i++) { + this.position = this.buffer.writeDoubleLE( + arr[i] as number, + this.position, + ); + } + break; + case null: + // empty array + break; + default: + throw new Error(`Unsupported array type [type=${type}]`); + } + } + } + + private static arraySize(dimensions: number[], type: ArrayPrimitive): number { + let numOfElements = 1; + for (let i = 0; i < dimensions.length; i++) { + numOfElements *= dimensions[i]; + } + + switch (type) { + case "number": + return numOfElements * 8; + case "boolean": + return numOfElements; + case "string": + // in case of string[] capacity check is done separately for each array element + return 0; + case null: + // empty array + return 0; + default: + throw new Error(`Unsupported array type [type=${type}]`); + } + } } export { SenderBufferV2 }; diff --git a/src/buffer/index.ts b/src/buffer/index.ts index 919871c..0cb1297 100644 --- a/src/buffer/index.ts +++ b/src/buffer/index.ts @@ -104,6 +104,8 @@ interface SenderBuffer { */ floatColumn(name: string, value: number): SenderBuffer; + arrayColumn(name: string, value: unknown[]): SenderBuffer; + /** * Writes an integer column with its value into the buffer. * diff --git a/src/sender.ts b/src/sender.ts index 1925682..ae92875 100644 --- a/src/sender.ts +++ b/src/sender.ts @@ -241,6 +241,11 @@ class Sender { return this; } + arrayColumn(name: string, value: unknown[]): Sender { + this.buffer.arrayColumn(name, value); + return this; + } + /** * Writes an integer column with its value into the buffer of the sender. * diff --git a/src/utils.ts b/src/utils.ts index 449ab4e..4141f20 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import { Agent } from "undici"; +type ArrayPrimitive = "number" | "boolean" | "string" | null; + type TimestampUnit = "ns" | "us" | "ms"; function isBoolean(value: unknown): value is boolean { @@ -38,6 +40,72 @@ function timestampToNanos(timestamp: bigint, unit: TimestampUnit) { } } +function getDimensions(data: unknown) { + const dimensions: number[] = []; + while (Array.isArray(data)) { + dimensions.push(data.length); + data = data[0]; + } + return dimensions; +} + +function validateArray(data: unknown[], dimensions: number[]): ArrayPrimitive { + if (data === null || data === undefined) { + return null; + } + if (!Array.isArray(data)) { + throw new Error( + `The value must be an array [value=${JSON.stringify(data)}, type=${typeof data}]`, + ); + } + + let expectedType: ArrayPrimitive = null; + + function checkArray( + array: unknown[], + depth: number = 0, + path: string = "", + ): void { + const expectedLength = dimensions[depth]; + if (array.length !== expectedLength) { + throw new Error( + `Lengths of sub-arrays do not match [expected=${expectedLength}, actual=${array.length}, dimensions=[${dimensions}], path=${path}]`, + ); + } + + if (depth < dimensions.length - 1) { + // intermediate level, expecting arrays + for (let i = 0; i < array.length; i++) { + if (!Array.isArray(array[i])) { + throw new Error( + `Mixed types found [expected=array, current=${typeof array[i]}, path=${path}[${i}]]`, + ); + } + checkArray(array[i] as unknown[], depth + 1, `${path}[${i}]`); + } + } else { + // leaf level, expecting primitives + if (expectedType === null && array[0] !== undefined) { + expectedType = typeof array[0] as ArrayPrimitive; + } + + for (let i = 0; i < array.length; i++) { + const currentType = typeof array[i] as ArrayPrimitive; + if (currentType !== expectedType) { + throw new Error( + expectedType !== null + ? `Mixed types found [expected=${expectedType}, current=${currentType}, path=${path}[${i}]]` + : `Unsupported array type [type=${currentType}]`, + ); + } + } + } + } + + checkArray(data); + return expectedType; +} + /** * Fetches JSON data from a URL. * @template T - The expected type of the JSON response @@ -83,4 +151,7 @@ export { timestampToNanos, TimestampUnit, fetchJson, + getDimensions, + validateArray, + ArrayPrimitive, }; diff --git a/test/sender.buffer.test.ts b/test/sender.buffer.test.ts index f941ab8..f9f5119 100644 --- a/test/sender.buffer.test.ts +++ b/test/sender.buffer.test.ts @@ -167,6 +167,281 @@ describe("Sender message builder test suite (anything not covered in client inte await sender.close(); }); + it("does not support arrays with protocol v1", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "1", + host: "host", + init_buf_size: 1024, + }); + expect(() => + sender.table("tableName").arrayColumn("arrayCol", [12.3, 23.4]), + ).toThrow("Arrays are not supported in protocol v1"); + await sender.close(); + }); + + it("supports arrays with protocol v2", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + await sender + .table("tableName") + .arrayColumn("arrayCol", [12.3, 23.4]) + .atNow(); + expect(bufferContentHex(sender)).toBe( + toHex("tableName arrayCol==") + + " 0e 0a 01 02 00 00 00 9a 99 99 99 99 99 28 40 66 66 66 66 66 66 37 40 " + + toHex("\n"), + ); + await sender.close(); + }); + + it("supports arrays with zeros", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + await sender.table("tableName").arrayColumn("arrayCol", [0.0, 0.0]).atNow(); + expect(bufferContentHex(sender)).toBe( + toHex("tableName arrayCol==") + + " 0e 0a 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " + + toHex("\n"), + ); + await sender.close(); + }); + + it("supports multidimensional arrays with protocol v2", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + await sender + .table("tableName") + .arrayColumn("arrayCol", [[12.3], [23.4]]) + .atNow(); + expect(bufferContentHex(sender)).toBe( + toHex("tableName arrayCol==") + + " 0e 0a 02 02 00 00 00 01 00 00 00 9a 99 99 99 99 99 28 40 66 66 66 66 66 66 37 40 " + + toHex("\n"), + ); + await sender.close(); + }); + + it("accepts empty array", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + await sender.table("tableName").arrayColumn("arrayCol", []).atNow(); + expect(bufferContentHex(sender)).toBe( + toHex("tableName arrayCol==") + " 0e 0a 01 00 00 00 00 " + toHex("\n"), + ); + await sender.close(); + }); + + it("accepts multi dimensional empty array", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + await sender + .table("tableName") + .arrayColumn("arrayCol", [ + [[], []], + [[], []], + [[], []], + ]) + .atNow(); + expect(bufferContentHex(sender)).toBe( + toHex("tableName arrayCol==") + + " 0e 0a 03 03 00 00 00 02 00 00 00 00 00 00 00 " + + toHex("\n"), + ); + await sender.close(); + }); + + it("does not accept irregularly sized array", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + expect(() => + sender.table("tableName").arrayColumn("arrayCol", [ + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [[1.1, 2.2], [3.3], [5.5, 6.6]], + ]), + ).toThrow( + "Lengths of sub-arrays do not match [expected=2, actual=1, dimensions=[4,3,2], path=[3][1]]", + ); + await sender.close(); + }); + + it("does not accept non-homogenous array", async function () { + const sender = new Sender({ + protocol: "tcp", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + sender.table("tableName"); + expect(() => + sender.arrayColumn("arrayCol", [ + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, "4.4"], + [5.5, 6.6], + ], + ]), + ).toThrow( + "Mixed types found [expected=number, current=string, path=[3][1][1]]", + ); + expect(() => + sender.arrayColumn("arrayCol", [ + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6], + ], + [[1.1, 2.2], 3.3, [5.5, 6.6]], + ]), + ).toThrow( + "Mixed types found [expected=array, current=number, path=[3][1]]", + ); + await sender.close(); + }); + + it("does not accept unsupported types", async function () { + const sender = new Sender({ + protocol: "http", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + sender.table("tableName"); + expect(() => sender.arrayColumn("col", ["str"])).toThrow( + "Unsupported array type [type=string]", + ); + expect(() => sender.arrayColumn("col", [true])).toThrow( + "Unsupported array type [type=boolean]", + ); + expect(() => sender.arrayColumn("col", [{}])).toThrow( + "Unsupported array type [type=object]", + ); + expect(() => sender.arrayColumn("col", [null])).toThrow( + "Unsupported array type [type=object]", + ); + expect(() => sender.arrayColumn("col", [undefined])).toThrow( + "Unsupported array type [type=undefined]", + ); + await sender.close(); + }); + + it("does not accept non-array types", async function () { + const sender = new Sender({ + protocol: "http", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + sender.table("tableName"); + // @ts-expect-error - Testing invalid input + expect(() => sender.arrayColumn("col", 12.345)).toThrow( + "The value must be an array [value=12.345, type=number]", + ); + // @ts-expect-error - Testing invalid input + expect(() => sender.arrayColumn("col", 42)).toThrow( + "The value must be an array [value=42, type=number]", + ); + // @ts-expect-error - Testing invalid input + expect(() => sender.arrayColumn("col", "str")).toThrow( + 'The value must be an array [value="str", type=string]', + ); + // @ts-expect-error - Testing invalid input + expect(() => sender.arrayColumn("col", "")).toThrow( + 'The value must be an array [value="", type=string]', + ); + // @ts-expect-error - Testing invalid input + expect(() => sender.arrayColumn("col", true)).toThrow( + "The value must be an array [value=true, type=boolean]", + ); + // @ts-expect-error - Testing invalid input + expect(() => sender.arrayColumn("col", {})).toThrow( + "The value must be an array [value={}, type=object]", + ); + await sender.close(); + }); + + it("supports arrays with NULL value", async function () { + const sender = new Sender({ + protocol: "http", + protocol_version: "2", + host: "host", + init_buf_size: 1024, + }); + await sender.table("tableName").arrayColumn("arrayCol", undefined).atNow(); + await sender.table("tableName").arrayColumn("arrayCol", null).atNow(); + expect(bufferContentHex(sender)).toBe( + toHex("tableName arrayCol==") + + " 0e 21 " + + toHex("\ntableName arrayCol==") + + " 0e 21 " + + toHex("\n"), + ); + await sender.close(); + }); + it("supports timestamp field as number", async function () { const sender = new Sender({ protocol: "tcp", diff --git a/test/sender.integration.test.ts b/test/sender.integration.test.ts index fd5a5c7..397c6c3 100644 --- a/test/sender.integration.test.ts +++ b/test/sender.integration.test.ts @@ -246,6 +246,278 @@ describe("Sender tests with containerized QuestDB instance", () => { await sender.close(); }); + it("can ingest data via HTTP with protocol v2", async () => { + const tableName = "test_http_v2"; + const schema = [ + { name: "location", type: "SYMBOL" }, + { name: "temperatures", type: "ARRAY", elemType: "DOUBLE", dim: 1 }, + { name: "timestamp", type: "TIMESTAMP" }, + ]; + + const sender = await Sender.fromConfig( + `http::addr=${container.getHost()}:${container.getMappedPort(QUESTDB_HTTP_PORT)};auto_flush_rows=1`, + ); + + // ingest via client + await sender + .table(tableName) + .symbol("location", "us") + .arrayColumn("temperatures", [17.1, 17.7, 18.4]) + .at(1658484765000000000n, "ns"); + + // wait for the table + await waitForTable(container, tableName); + + // query table + const select1Result = await runSelect(container, tableName, 1); + expect(select1Result.query).toBe(tableName); + expect(select1Result.count).toBe(1); + expect(select1Result.columns).toStrictEqual(schema); + expect(select1Result.dataset).toStrictEqual([ + ["us", [17.1, 17.7, 18.4], "2022-07-22T10:12:45.000000Z"], + ]); + + // ingest via client, add new columns + await sender + .table(tableName) + .symbol("location", "us") + .arrayColumn("temperatures", [17.36, 18.4, 19.6, 18.7]) + .at(1658484765000666000n, "ns"); + await sender + .table(tableName) + .symbol("location", "emea") + .symbol("city", "london") + .arrayColumn("temperatures", [18.5, 18.4, 19.2]) + .floatColumn("daily_avg_temp", 18.7) + .at(1658484765001234000n, "ns"); + + // query table + const select2Result = await runSelect(container, tableName, 3); + expect(select2Result.query).toBe(tableName); + expect(select2Result.count).toBe(3); + expect(select2Result.columns).toStrictEqual([ + { name: "location", type: "SYMBOL" }, + { name: "temperatures", type: "ARRAY", elemType: "DOUBLE", dim: 1 }, + { name: "timestamp", type: "TIMESTAMP" }, + { name: "city", type: "SYMBOL" }, + { name: "daily_avg_temp", type: "DOUBLE" }, + ]); + expect(select2Result.dataset).toStrictEqual([ + ["us", [17.1, 17.7, 18.4], "2022-07-22T10:12:45.000000Z", null, null], + [ + "us", + [17.36, 18.4, 19.6, 18.7], + "2022-07-22T10:12:45.000666Z", + null, + null, + ], + [ + "emea", + [18.5, 18.4, 19.2], + "2022-07-22T10:12:45.001234Z", + "london", + 18.7, + ], + ]); + + await sender.close(); + }); + + it("can ingest NULL array via HTTP with protocol v2", async () => { + const tableName = "test_http_v2_null"; + const schema = [ + { name: "location", type: "SYMBOL" }, + { name: "temperatures", type: "ARRAY", elemType: "DOUBLE", dim: 1 }, + { name: "timestamp", type: "TIMESTAMP" }, + ]; + + const sender = await Sender.fromConfig( + `http::addr=${container.getHost()}:${container.getMappedPort(QUESTDB_HTTP_PORT)}`, + ); + + // ingest via client + await sender + .table(tableName) + .symbol("location", "us") + .arrayColumn("temperatures", [17.1, 17.7, 18.4]) + .at(1658484765000000000n, "ns"); + await sender + .table(tableName) + .symbol("location", "gb") + .at(1658484765000666000n, "ns"); + await sender.flush(); + + // wait for the table + await waitForTable(container, tableName); + + // query table + const select1Result = await runSelect(container, tableName, 2); + expect(select1Result.query).toBe(tableName); + expect(select1Result.count).toBe(2); + expect(select1Result.columns).toStrictEqual(schema); + expect(select1Result.dataset).toStrictEqual([ + ["us", [17.1, 17.7, 18.4], "2022-07-22T10:12:45.000000Z"], + ["gb", null, "2022-07-22T10:12:45.000666Z"], + ]); + + await sender.close(); + }); + + it("can ingest zero vector via HTTP with protocol v2", async () => { + const tableName = "test_http_v2_zeros"; + const schema = [ + { name: "location", type: "SYMBOL" }, + { name: "temperatures", type: "ARRAY", elemType: "DOUBLE", dim: 2 }, + { name: "timestamp", type: "TIMESTAMP" }, + ]; + + const sender = await Sender.fromConfig( + `http::addr=${container.getHost()}:${container.getMappedPort(QUESTDB_HTTP_PORT)}`, + ); + + // ingest via client + await sender + .table(tableName) + .symbol("location", "us") + .arrayColumn("temperatures", [ + [17.1, 17.7, 18.4], + [17.1, 17.7, 18.4], + ]) + .at(1658484765000000000n, "ns"); + await sender + .table(tableName) + .symbol("location", "gb") + .arrayColumn("temperatures", [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ]) + .at(1658484765000666000n, "ns"); + await sender.flush(); + + // wait for the table + await waitForTable(container, tableName); + + // query table + const select1Result = await runSelect(container, tableName, 2); + expect(select1Result.query).toBe(tableName); + expect(select1Result.count).toBe(2); + expect(select1Result.columns).toStrictEqual(schema); + expect(select1Result.dataset).toStrictEqual([ + [ + "us", + [ + [17.1, 17.7, 18.4], + [17.1, 17.7, 18.4], + ], + "2022-07-22T10:12:45.000000Z", + ], + [ + "gb", + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ], + "2022-07-22T10:12:45.000666Z", + ], + ]); + + await sender.close(); + }); + + it("can ingest empty array via HTTP with protocol v2", async () => { + const tableName = "test_http_v2_empty"; + const schema = [ + { name: "location", type: "SYMBOL" }, + { name: "temperatures", type: "ARRAY", elemType: "DOUBLE", dim: 1 }, + { name: "timestamp", type: "TIMESTAMP" }, + ]; + + const sender = await Sender.fromConfig( + `http::addr=${container.getHost()}:${container.getMappedPort(QUESTDB_HTTP_PORT)}`, + ); + + // ingest via client + await sender + .table(tableName) + .symbol("location", "us") + .arrayColumn("temperatures", [17.1, 17.7, 18.4]) + .at(1658484765000000000n, "ns"); + await sender + .table(tableName) + .symbol("location", "gb") + .arrayColumn("temperatures", []) + .at(1658484765000666000n, "ns"); + await sender.flush(); + + // wait for the table + await waitForTable(container, tableName); + + // query table + const select1Result = await runSelect(container, tableName, 2); + expect(select1Result.query).toBe(tableName); + expect(select1Result.count).toBe(2); + expect(select1Result.columns).toStrictEqual(schema); + expect(select1Result.dataset).toStrictEqual([ + ["us", [17.1, 17.7, 18.4], "2022-07-22T10:12:45.000000Z"], + ["gb", [], "2022-07-22T10:12:45.000666Z"], + ]); + + await sender.close(); + }); + + it("can ingest multi dimensional empty array via HTTP with protocol v2", async () => { + const tableName = "test_http_v2_multi_empty"; + const schema = [ + { name: "location", type: "SYMBOL" }, + { name: "temperatures", type: "ARRAY", elemType: "DOUBLE", dim: 2 }, + { name: "timestamp", type: "TIMESTAMP" }, + ]; + + const sender = await Sender.fromConfig( + `http::addr=${container.getHost()}:${container.getMappedPort(QUESTDB_HTTP_PORT)}`, + ); + + // ingest via client + await sender + .table(tableName) + .symbol("location", "us") + .arrayColumn("temperatures", [ + [17.1, 17.7], + [18.4, 18.7], + ]) + .at(1658484765000000000n, "ns"); + await sender + .table(tableName) + .symbol("location", "gb") + .arrayColumn("temperatures", [[], []]) + .at(1658484765000666000n, "ns"); + await sender.flush(); + + // wait for the table + await waitForTable(container, tableName); + + // query table + const select1Result = await runSelect(container, tableName, 2); + expect(select1Result.query).toBe(tableName); + expect(select1Result.count).toBe(2); + expect(select1Result.columns).toStrictEqual(schema); + expect(select1Result.dataset).toStrictEqual([ + [ + "us", + [ + [17.1, 17.7], + [18.4, 18.7], + ], + "2022-07-22T10:12:45.000000Z", + ], + // todo: this should be [[], []] + // probably a server bug + ["gb", [], "2022-07-22T10:12:45.000666Z"], + ]); + + await sender.close(); + }, 60000000); + it("can ingest data via HTTP with auto flush interval", async () => { const tableName = "test_http_interval"; const schema = [