Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/client/ColumnDecoder.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,8 +17,7 @@
* under the License.
*/

import { logger } from "../utils/Logger";
import { parseIntToDate } from "../utils/DataTypes";
import { objectBytesToString, parseIntToDate } from "../utils/DataTypes";

/**
* Column encoding types matching Apache IoTDB ColumnEncoding enum
Expand DownExpand Up@@ -323,7 +322,7 @@ class ByteArrayColumnDecoder implements ColumnDecoder {

/**
* Decoder for BINARY array encoding (encoding=3)
* Handles TEXT, STRING, and BLOB data types with variable-length values
* Handles TEXT, STRING, BLOB, and OBJECT data types with variable-length values
*/
class BinaryArrayColumnDecoder implements ColumnDecoder {
readColumn(
Expand All@@ -332,8 +331,8 @@ class BinaryArrayColumnDecoder implements ColumnDecoder {
dataType: number,
positionCount: number,
): { column: Column; bytesRead: number } {
// Supports TEXT(5), BLOB(10), STRING(11)
if (dataType !== 5 && dataType !== 10 && dataType !== 11) {
// Supports TEXT(5), BLOB(10), STRING(11), OBJECT(12)
if (dataType !== 5 && dataType !== 10 && dataType !== 11 && dataType !== 12) {
throw new Error(
`Invalid data type ${dataType} for BinaryArrayColumnDecoder`,
);
Expand DownExpand Up@@ -370,6 +369,9 @@ class BinaryArrayColumnDecoder implements ColumnDecoder {
if (dataType === 10) {
// BLOB - keep as Buffer
values[i] = data;
} else if (dataType === 12) {
// OBJECT - render as "(Object) 1.00 KB" like the Go/C# clients
values[i] = objectBytesToString(data);
} else {
// TEXT/STRING - convert to UTF-8 string
values[i] = data.toString("utf8");
Expand Down
96 changes: 85 additions & 11 deletions src/client/Session.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,12 +29,12 @@ import { registerClosable, unregisterClosable } from "../utils/ProcessCleanup";
import { SessionDataSet } from "./SessionDataSet";
import { RowRecord } from "./RowRecord";
import { BaseColumnDecoder, ColumnEncoding, Column } from "./ColumnDecoder";
import { RedirectException, isWildcardAddress } from "../utils/Errors";
import { isWildcardAddress } from "../utils/Errors";
import {
serializeTabletValuesFast,
serializeTimestamps
} from "../utils/FastSerializer";
import { parseDateToInt, parseIntToDate } from "../utils/DataTypes";
import { parseDateToInt, parseIntToDate, TSDataType } from "../utils/DataTypes";

const ttypes = require("../thrift/generated/client_types");

Expand DownExpand Up@@ -172,6 +172,75 @@ export class TableTablet implements ITableTablet {
this.timestamps.push(timestamp);
this.values.push(values);
}

/**
* Build the wire representation of one OBJECT segment: a 1-byte isEOF
* flag, an 8-byte big-endian offset, then the raw segment content.
* This matches Java's
* Tablet.addValue(rowIndex, columnIndex, isEOF, offset, content).
*
* @param isEOF - Whether this segment is the last one of the object
* @param offset - Byte offset of this segment within the whole object
* @param content - Raw bytes of this segment
*/
static buildObjectValue(
isEOF: boolean,
offset: number | bigint,
content: Buffer | Uint8Array,
): Buffer {
let bigintOffset = typeof offset === "bigint" ? offset : BigInt(offset);
if (
typeof offset === "number" &&
(!Number.isSafeInteger(offset) || offset < 0)
) {
throw new Error(`Invalid OBJECT segment offset: ${offset}`);
}
if (bigintOffset < 0n) {
throw new Error(`Invalid OBJECT segment offset: ${offset}`);
}
const raw = Buffer.isBuffer(content) ? content : Buffer.from(content);
const value = Buffer.allocUnsafe(9 + raw.length);
value[0] = isEOF ? 1 : 0;
value.writeBigUInt64BE(bigintOffset, 1);
raw.copy(value, 9);
return value;
}

/**
* Write one segment of an OBJECT column value at an existing row.
*
* An OBJECT value can be written in multiple segments so a large object
* does not need to be fully loaded into memory. Segments must be written
* with ascending offsets and the last segment must set isEOF to true.
*
* @param isEOF - Whether this segment is the last one of the object
* @param offset - Byte offset of this segment within the whole object
* @param content - Raw bytes of this segment
* @param columnIndex - Index of the OBJECT column
* @param rowIndex - Index of the row to write into
*/
setObjectValueAt(
isEOF: boolean,
offset: number | bigint,
content: Buffer | Uint8Array,
columnIndex: number,
rowIndex: number,
): void {
if (columnIndex < 0 || columnIndex >= this.columnTypes.length) {
throw new Error(`Illegal columnIndex: ${columnIndex}`);
}
if (this.columnTypes[columnIndex] !== TSDataType.OBJECT) {
throw new Error(`Column ${columnIndex} must be of type OBJECT`);
}
if (rowIndex < 0 || rowIndex >= this.values.length) {
throw new Error(`Illegal rowIndex: ${rowIndex}`);
}
this.values[rowIndex][columnIndex] = TableTablet.buildObjectValue(
isEOF,
offset,
content,
);
}
}

/**
Expand DownExpand Up@@ -760,8 +829,9 @@ export class Session {
});
return buffer;
}
case 10: {
// BLOB
case 10: // BLOB
case 12: {
// OBJECT (table model) uses the same binary length-prefix encoding as BLOB
// Optimized: Pre-calculate total size to avoid multiple Buffer.concat calls

// Phase 1: Convert all values to buffers and calculate total size
Expand DownExpand Up@@ -1152,6 +1222,7 @@ export class Session {
else if (type.includes("DATE")) return 9;
else if (type.includes("BLOB")) return 10;
else if (type.includes("STRING")) return 11;
else if (type.includes("OBJECT")) return 12;
return 5; // Default to TEXT
}

Expand All@@ -1178,15 +1249,18 @@ export class Session {
case 5: // TEXT - variable length, need to parse
case 10: // BLOB - variable length
case 11: // STRING - variable length
case 12: // OBJECT - variable length (binary, length-prefixed)
// For variable-length types, count entries by parsing length prefixes
let count = 0;
let offset = 0;
while (offset + 4 <= length) {
const strLength = buffer.readInt32BE(offset);
offset += 4 + strLength;
count++;
{
let count = 0;
let offset = 0;
while (offset + 4 <= length) {
const strLength = buffer.readInt32BE(offset);
offset += 4 + strLength;
count++;
}
return count;
}
return count;
default:
logger.warn(
`Unknown data type ${dataType}, cannot determine row count`,
Expand Down
47 changes: 46 additions & 1 deletion src/utils/DataTypes.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,13 @@ export enum TSDataType {
*/
STRING = 11,

// OBJECT = 12, // Reserved - not yet implemented
/**
* OBJECT (table model only)
* JavaScript type: Buffer (built with the segment framing: 1-byte isEOF +
* 8-byte big-endian offset + raw content)
* Storage size: variable (4-byte length prefix + binary content)
*/
OBJECT = 12,
}

/**
Expand DownExpand Up@@ -241,7 +247,46 @@ export function getDataTypeName(typeCode: number): string {
return "BLOB";
case TSDataType.STRING:
return "STRING";
case TSDataType.OBJECT:
return "OBJECT";
default:
return "UNKNOWN";
}
}

/**
* Format the wire representation of a stored OBJECT value for display.
*
* The server stores OBJECT cells as an 8-byte big-endian file size followed
* by the internal object path. Mirroring the Go client's objectBytesToString,
* this helper renders the size in human-readable units:
* "(Object) 1023 B", "(Object) 1.00 KB", "(Object) 1.00 MB",
* "(Object) 1.00 GB".
*
* @param input - Raw OBJECT cell bytes (at least the 8-byte size prefix)
* @returns Human-readable size string
* @throws Error if the input is shorter than 8 bytes
*/
export function objectBytesToString(input: Buffer | Uint8Array): string {
if (input.length < 8) {
throw new Error(
"Invalid OBJECT value: expected at least 8 bytes, got " + input.length,
);
}
const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
const size = buffer.readUInt32BE(0) * 0x100000000 + buffer.readUInt32BE(4);
const KILOBYTE = 1024;
const MEGABYTE = KILOBYTE * 1024;
const GIGABYTE = MEGABYTE * 1024;

if (size < KILOBYTE) {
return "(Object) " + size + " B";
}
if (size < MEGABYTE) {
return "(Object) " + (size / KILOBYTE).toFixed(2) + " KB";
}
if (size < GIGABYTE) {
return "(Object) " + (size / MEGABYTE).toFixed(2) + " MB";
}
return "(Object) " + (size / GIGABYTE).toFixed(2) + " GB";
}
7 changes: 5 additions & 2 deletions src/utils/FastSerializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,6 +299,7 @@ export function serializeColumnFast(values: any[], dataType: number): Buffer {
case 9: // DATE
return serializeDateColumn(values);
case 10: // BLOB
case 12: // OBJECT (same length-prefixed binary encoding)
return serializeBlobColumn(values);
default:
throw new Error(`Unsupported data type: ${dataType}`);
Expand DownExpand Up@@ -370,7 +371,8 @@ export function serializeTabletValuesFast(
}
break;
}
case 10: { // BLOB
case 10: // BLOB
case 12: { // OBJECT (same length-prefixed binary encoding)
for (let r = 0; r < rowCount; r++) {
const v = values[r][c];
dataSize += 4 + (v === null || v === undefined ? 0 : blobByteLength(v));
Expand DownExpand Up@@ -517,7 +519,8 @@ export function serializeTabletValuesFast(
off += 4;
}
break;
case 10: { // BLOB
case 10: // BLOB
case 12: { // OBJECT (same length-prefixed binary encoding)
for (let r = 0; r < rowCount; r++) {
const v = values[r][c];
if (v === null || v === undefined) {
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/DataTypes.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,11 @@
*/

import {
getDataTypeName,
objectBytesToString,
parseDateToInt,
parseIntToDate,
TSDataType,
} from "../../src/utils/DataTypes";
import {
BaseColumnDecoder,
Expand DownExpand Up@@ -132,6 +135,51 @@ describe("DATE yyyyMMdd conversion", () => {
});
});

describe("OBJECT type codes and display formatting", () => {
it("exposes OBJECT as TSDataType 12 and resolves its name", () => {
expect(TSDataType.OBJECT).toBe(12);
expect(getDataTypeName(12)).toBe("OBJECT");
});

it.each([
[1023, "(Object) 1023 B"],
[1024, "(Object) 1.00 KB"],
[1024 * 1024, "(Object) 1.00 MB"],
[1024 * 1024 * 1024, "(Object) 1.00 GB"],
] as const)("formats %d bytes as %s", (size, expected) => {
const buffer = Buffer.alloc(8 + 4);
buffer.writeUInt32BE(Math.floor(size / 0x100000000), 0);
buffer.writeUInt32BE(size >>> 0, 4);
expect(objectBytesToString(buffer)).toBe(expected);
});

it("rejects OBJECT values shorter than the 8-byte size prefix", () => {
expect(() => objectBytesToString(Buffer.alloc(7))).toThrow(
/expected at least 8 bytes/,
);
});
});

describe("TsBlock column decode (BinaryArrayColumnDecoder for OBJECT)", () => {
it("decodes an OBJECT cell as a human-readable size string", () => {
// BinaryArray column: mayHaveNull=0, length i32 BE, 8-byte size + path.
const payload = Buffer.alloc(8 + "internal/path/1.bin".length);
payload.writeUInt32BE(0, 0);
payload.writeUInt32BE(1024, 4);
payload.write("internal/path/1.bin", 8, "utf8");

const buffer = Buffer.concat([
Buffer.from([0x00]), // mayHaveNull
Buffer.from([0, 0, 0, payload.length]),
payload,
]);

const decoder = BaseColumnDecoder.getDecoder(ColumnEncoding.BinaryArray);
const { column } = decoder.readColumn(buffer, 0, TSDataType.OBJECT, 1);
expect(column.values[0]).toBe("(Object) 1.00 KB");
});
});

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
Expand Down
Loading
Loading