blob: fd300a27fe5f0aef6a9b9066818991f895314921 [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
parseDateToInt,
parseIntToDate,
} from "../../src/utils/DataTypes";
import {
BaseColumnDecoder,
ColumnEncoding,
} from "../../src/client/ColumnDecoder";
import { Session } from "../../src/client/Session";
describe("DATE yyyyMMdd conversion", () => {
describe("parseDateToInt", () => {
it("should encode a Date as INT32 yyyyMMdd (year*10000 + month*100 + day)", () => {
expect(parseDateToInt(new Date("2026-07-13"))).toBe(20260713);
expect(parseDateToInt(new Date("2024-01-01"))).toBe(20240101);
expect(parseDateToInt(new Date("2024-12-31"))).toBe(20241231);
// Leap day
expect(parseDateToInt(new Date("2024-02-29"))).toBe(20240229);
});
it("should pass through already-encoded valid numbers unchanged", () => {
expect(parseDateToInt(20260713)).toBe(20260713);
expect(parseDateToInt(10000101)).toBe(10000101); // year lower bound
expect(parseDateToInt(99991231)).toBe(99991231); // year upper bound
});
it("should accept the leap day of a leap year", () => {
expect(parseDateToInt(20240229)).toBe(20240229);
expect(parseDateToInt(new Date("2024-02-29"))).toBe(20240229);
});
it("should reject invalid Date objects", () => {
expect(() => parseDateToInt(new Date(NaN))).toThrow(/Invalid DATE/);
});
it("should reject Date objects with years outside 1000-9999", () => {
expect(() => parseDateToInt(new Date("0999-12-31"))).toThrow(
/year 999/,
);
});
it("should reject non-finite and non-integer numbers", () => {
expect(() => parseDateToInt(NaN)).toThrow(/Invalid DATE/);
expect(() => parseDateToInt(Infinity)).toThrow(/Invalid DATE/);
expect(() => parseDateToInt(20240101.5)).toThrow(/Invalid DATE/);
});
it("should reject numbers that are not real calendar dates", () => {
expect(() => parseDateToInt(20230229)).toThrow(/day 29/); // not a leap year
expect(() => parseDateToInt(20241301)).toThrow(/month 13/);
expect(() => parseDateToInt(20240100)).toThrow(/day 0/);
expect(() => parseDateToInt(9991231)).toThrow(/year 999/);
expect(() => parseDateToInt(100000101)).toThrow(/year 10000/);
// Old days-since-epoch value: 19723 -> year 1, month 97 -> invalid
expect(() => parseDateToInt(19723)).toThrow(/Invalid DATE/);
expect(() => parseDateToInt(0)).toThrow(/Invalid DATE/);
});
});
describe("parseIntToDate", () => {
it("should decode INT32 yyyyMMdd to a Date at UTC midnight", () => {
const date = parseIntToDate(20260713);
expect(date.getUTCFullYear()).toBe(2026);
expect(date.getUTCMonth()).toBe(6); // July (0-based)
expect(date.getUTCDate()).toBe(13);
expect(date.getUTCHours()).toBe(0);
expect(date.getUTCMinutes()).toBe(0);
});
it("should decode the leap day of a leap year", () => {
const date = parseIntToDate(20240229);
expect(date.getUTCFullYear()).toBe(2024);
expect(date.getUTCMonth()).toBe(1);
expect(date.getUTCDate()).toBe(29);
});
it("should reject impossible calendar dates instead of normalizing", () => {
// new Date(Date.UTC(2023, 1, 29)) would silently become 2023-03-01
expect(() => parseIntToDate(20230229)).toThrow(/day 29/);
expect(() => parseIntToDate(20241301)).toThrow(/month 13/);
expect(() => parseIntToDate(20240100)).toThrow(/day 0/);
});
it("should reject years outside 1000-9999", () => {
expect(() => parseIntToDate(9991231)).toThrow(/year 999/);
expect(() => parseIntToDate(100000101)).toThrow(/year 10000/);
// Old days-since-epoch value is not a valid yyyyMMdd
expect(() => parseIntToDate(19723)).toThrow(/Invalid DATE/);
});
it("should reject non-integer numbers", () => {
expect(() => parseIntToDate(NaN)).toThrow(/Invalid DATE/);
expect(() => parseIntToDate(20240101.5)).toThrow(/Invalid DATE/);
});
});
describe("round-trip", () => {
it("should round-trip Date -> yyyyMMdd -> Date", () => {
const dates = [
new Date("1970-01-01"),
new Date("2000-02-29"),
new Date("2024-01-01"),
new Date("2026-07-13"),
new Date("9999-12-31"),
];
for (const original of dates) {
const encoded = parseDateToInt(original);
const decoded = parseIntToDate(encoded);
expect(decoded.getTime()).toBe(original.getTime());
// And the integer round-trips too
expect(parseDateToInt(decoded)).toBe(encoded);
}
});
});
describe("TsBlock column decode (Int32ArrayColumnDecoder)", () => {
it("should decode a DATE column value as a Date from yyyyMMdd wire bytes", () => {
// Column layout: 1 byte null flag (0 = no nulls) + INT32 BE values
// 20260713 = 0x01352769
const buffer = Buffer.from([0x00, 0x01, 0x35, 0x27, 0x69]);
const decoder = BaseColumnDecoder.getDecoder(ColumnEncoding.Int32Array);
const { column, bytesRead } = decoder.readColumn(buffer, 0, 9, 1);
expect(bytesRead).toBe(5);
const value = column.values[0];
expect(value).toBeInstanceOf(Date);
expect(value.getUTCFullYear()).toBe(2026);
expect(value.getUTCMonth()).toBe(6);
expect(value.getUTCDate()).toBe(13);
});
it("should still decode plain INT32 columns as numbers", () => {
const buffer = Buffer.from([0x00, 0x01, 0x35, 0x27, 0x69]);
const decoder = BaseColumnDecoder.getDecoder(ColumnEncoding.Int32Array);
const { column } = decoder.readColumn(buffer, 0, 1, 1);
expect(column.values[0]).toBe(20260713);
});
});
describe("parseTsBlock DATE conversion with columnIndex2TsBlockColumnIndexList", () => {
let session: Session;
beforeEach(() => {
session = new Session({
host: "localhost",
port: 6667,
username: "root",
password: "root",
});
});
/**
* Craft a TsBlock buffer with INT32 (wire type 1) value columns:
* valueColumnCount i32 BE | type bytes | positionCount i32 BE |
* time encoding byte | value encoding bytes | time column | value columns
* Each column: 1-byte null flag (0 = no nulls) + big-endian values.
*/
function craftInt32TsBlock(
timestamps: number[],
int32Columns: number[][],
): Buffer {
const positionCount = timestamps.length;
const parts: Buffer[] = [];
const header = Buffer.alloc(4);
header.writeInt32BE(int32Columns.length, 0);
parts.push(header);
// Wire type of every value column is INT32 (1)
parts.push(Buffer.from(int32Columns.map(() => 1)));
const posBuf = Buffer.alloc(4);
posBuf.writeInt32BE(positionCount, 0);
parts.push(posBuf);
// Encodings: time column Int64Array (2), value columns Int32Array (1)
parts.push(Buffer.from([2, ...int32Columns.map(() => 1)]));
// Time column: null flag + INT64 BE timestamps
const timeBuf = Buffer.alloc(1 + positionCount * 8);
timeBuf.writeUInt8(0, 0);
timestamps.forEach((t, i) => {
timeBuf.writeBigInt64BE(BigInt(t), 1 + i * 8);
});
parts.push(timeBuf);
// Value columns: null flag + INT32 BE values
for (const columnValues of int32Columns) {
const colBuf = Buffer.alloc(1 + positionCount * 4);
colBuf.writeUInt8(0, 0);
columnValues.forEach((v, i) => {
colBuf.writeInt32BE(v, 1 + i * 4);
});
parts.push(colBuf);
}
return Buffer.concat(parts);
}
it("should convert the DATE column per the non-identity logical->physical mapping", async () => {
// Logical columns: [DATE, INT32]; physical TsBlock columns reordered:
// logical 0 (DATE) -> physical 1, logical 1 (INT32) -> physical 0
const buffer = craftInt32TsBlock([1000], [[42], [20240101]]);
const rows = await (session as any).parseQueryResult(
[buffer],
2,
["DATE", "INT32"],
false,
[1, 0],
);
expect(rows).toHaveLength(1);
const [timestamp, physical0, physical1] = rows[0];
expect(Number(timestamp)).toBe(1000);
// Physical column 0 is logically INT32 -> stays a number
expect(physical0).toBe(42);
// Physical column 1 is logically DATE -> converted to Date
expect(physical1).toBeInstanceOf(Date);
expect(physical1.getUTCFullYear()).toBe(2024);
expect(physical1.getUTCMonth()).toBe(0);
expect(physical1.getUTCDate()).toBe(1);
});
it("should keep identity behavior when the mapping is absent", async () => {
const buffer = craftInt32TsBlock([1000], [[20240101], [42]]);
const rows = await (session as any).parseQueryResult(
[buffer],
2,
["DATE", "INT32"],
false,
);
expect(rows).toHaveLength(1);
expect(rows[0][1]).toBeInstanceOf(Date);
expect(rows[0][1].getUTCFullYear()).toBe(2024);
expect(rows[0][2]).toBe(42);
});
it("should handle deduplicated columns (two logical columns -> one physical)", async () => {
// SELECT d, d: logical [DATE, DATE] both map to physical 0
const buffer = craftInt32TsBlock([1000], [[20240101]]);
const rows = await (session as any).parseQueryResult(
[buffer],
2,
["DATE", "DATE"],
false,
[0, 0],
);
expect(rows).toHaveLength(1);
expect(rows[0][1]).toBeInstanceOf(Date);
expect(rows[0][1].getUTCFullYear()).toBe(2024);
});
it("should not crash nor convert when logical types disagree for one physical column", async () => {
const buffer = craftInt32TsBlock([1000], [[20240101]]);
const rows = await (session as any).parseQueryResult(
[buffer],
2,
["DATE", "INT32"],
false,
[0, 0],
);
expect(rows).toHaveLength(1);
// Ambiguous type -> no DATE conversion applied
expect(rows[0][1]).toBe(20240101);
});
});
});