Skip to content
Merged
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
6 changes: 3 additions & 3 deletions docs/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The IoTDB Node.js client supports all standard IoTDB data types as defined in Ap
| 6 | VECTOR | Vector data (not yet implemented) | - | - |
| 7 | UNKNOWN | Unknown type (reserved) | - | - |
| 8 | TIMESTAMP | Timestamp (milliseconds) | `Date` | 8 bytes |
| 9 | DATE | Date (days since epoch) | `Date` | 4 bytes |
| 9 | DATE | Calendar date (INT32 `yyyyMMdd`, e.g. `20240101` for 2024-01-01) | `Date` | 4 bytes |
| 10 | BLOB | Binary data | `Buffer` | Variable (4-byte length + content) |
| 11 | STRING | UTF-8 encoded string | `string` | Variable (4-byte length + content) |
| 12 | OBJECT | Object type (reserved) | - | - |
Expand Down Expand Up @@ -176,7 +176,7 @@ for (const row of result.rows) {
| `bigint` | INT64, TIMESTAMP | Direct mapping |
| `string` | TEXT, STRING | UTF-8 encoded |
| `Buffer` | BLOB | Binary data |
| `Date` | DATE, TIMESTAMP | Converted to days or milliseconds |
| `Date` | DATE, TIMESTAMP | DATE: encoded as `yyyyMMdd` integer (e.g. `20240101`); TIMESTAMP: milliseconds since epoch |

### IoTDB to JavaScript

Expand All @@ -190,7 +190,7 @@ for (const row of result.rows) {
| TEXT | `string` | UTF-8 decoded |
| BLOB | `Buffer` | Raw binary data |
| STRING | `string` | UTF-8 decoded |
| DATE | `Date` | Days since epoch converted to Date |
| DATE | `Date` | `yyyyMMdd` integer (e.g. `20240101`) converted to Date at UTC midnight |
| TIMESTAMP | `Date` | Milliseconds since epoch |

## Null Values
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide-tree-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ const pool = new SessionPool(poolConfig);
| 4 | DOUBLE | number | 64 位浮点数 |
| 5 | TEXT | string | UTF-8 字符串 |
| 8 | TIMESTAMP | number/Date | 自纪元以来的毫秒数 |
| 9 | DATE | number/Date | 自纪元以来的天数 |
| 9 | DATE | number/Date | 日历日期,`yyyyMMdd` 整数(例如 `20240101` 表示 2024-01-01)|
| 10 | BLOB | Buffer | 二进制数据 |
| 11 | STRING | string | 与 TEXT 相同 |

Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide-tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ The tree model supports all IoTDB data types:
| 4 | DOUBLE | number | 64-bit floating point |
| 5 | TEXT | string | UTF-8 string |
| 8 | TIMESTAMP | number/Date | Milliseconds since epoch |
| 9 | DATE | number/Date | Days since epoch |
| 9 | DATE | number/Date | Calendar date as `yyyyMMdd` integer (e.g. `20240101` for 2024-01-01) |
| 10 | BLOB | Buffer | Binary data |
| 11 | STRING | string | Same as TEXT |

Expand Down
13 changes: 12 additions & 1 deletion src/client/ColumnDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

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

/**
* Column encoding types matching Apache IoTDB ColumnEncoding enum
Expand Down Expand Up @@ -152,7 +153,6 @@ class Int32ArrayColumnDecoder implements ColumnDecoder {

switch (dataType) {
case 1: // INT32
case 9: // DATE
for (let i = 0; i < positionCount; i++) {
if (nullIndicators && nullIndicators[i]) {
values[i] = null;
Expand All @@ -163,6 +163,17 @@ class Int32ArrayColumnDecoder implements ColumnDecoder {
}
break;

case 9: // DATE (INT32 yyyyMMdd encoding -> Date object)
for (let i = 0; i < positionCount; i++) {
if (nullIndicators && nullIndicators[i]) {
values[i] = null;
continue;
}
values[i] = parseIntToDate(buffer.readInt32BE(currentOffset));
currentOffset += 4;
}
break;

case 3: // FLOAT
for (let i = 0; i < positionCount; i++) {
if (nullIndicators && nullIndicators[i]) {
Expand Down
112 changes: 95 additions & 17 deletions src/client/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
serializeTimestamps
} from "../utils/FastSerializer";
import { globalBufferPool } from "../utils/BufferPool";
import { parseDateToInt, parseIntToDate } from "../utils/DataTypes";

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

Expand Down Expand Up @@ -318,6 +319,7 @@ export class Session {
response.columns?.length || 0,
response.dataTypeList || [],
ignoreTimeStamp,
response.columnIndex2TsBlockColumnIndexList,
);
} else if (response.queryDataSet) {
// Old columnar format (TSQueryDataSet)
Expand Down Expand Up @@ -741,18 +743,11 @@ export class Session {
return buffer;
}
case 9: {
// DATE (stored as INT32 - days since epoch) - Use big-endian
// DATE (stored as INT32 - yyyyMMdd, e.g. 20240101) - Use big-endian
const buffer = Buffer.alloc(values.length * 4);
values.forEach((v, i) => {
let days = 0;
if (v !== null && v !== undefined) {
if (v instanceof Date) {
days = Math.floor(v.getTime() / (24 * 60 * 60 * 1000));
} else {
days = v;
}
}
buffer.writeInt32BE(days, i * 4);
const encoded = v === null || v === undefined ? 0 : parseDateToInt(v);
buffer.writeInt32BE(encoded, i * 4);
});
return buffer;
}
Expand Down Expand Up @@ -888,12 +883,16 @@ export class Session {
* - Value columns data
*
* @param ignoreTimeStamp - If true, no time column is present
* @param columnIndex2TsBlockColumnIndexList - Server-provided mapping from
* LOGICAL response column index to PHYSICAL TsBlock column index (-1 =
* time column); identity mapping is assumed when absent
*/
async parseQueryResult(
queryResult: Buffer[],
_columnCount: number,
dataTypes: string[],
ignoreTimeStamp: boolean = false,
columnIndex2TsBlockColumnIndexList?: number[],
): Promise<any[][]> {
const rows: any[][] = [];

Expand All @@ -907,6 +906,14 @@ export class Session {
);
logger.debug(`parseQueryResult: dataTypes: ${JSON.stringify(dataTypes)}`);

// dataTypes is ordered by LOGICAL response columns; physical TsBlock
// columns may be deduplicated/reordered. Derive each PHYSICAL column's
// logical type once so parseTsBlock can convert DATE columns correctly.
const physicalColumnTypes = this.derivePhysicalColumnTypes(
dataTypes,
columnIndex2TsBlockColumnIndexList,
);

// Process each TsBlock in queryResult
for (let blockIndex = 0; blockIndex < queryResult.length; blockIndex++) {
const tsBlockBuffer = Buffer.isBuffer(queryResult[blockIndex])
Expand All @@ -920,7 +927,7 @@ export class Session {
try {
const blockRows = this.parseTsBlock(
tsBlockBuffer,
dataTypes,
physicalColumnTypes,
ignoreTimeStamp,
);
rows.push(...blockRows);
Expand All @@ -934,6 +941,55 @@ export class Session {
return rows;
}

/**
* Derive the per-PHYSICAL-TsBlock-column logical data types from the
* logically-ordered dataTypeList and the server-provided
* columnIndex2TsBlockColumnIndexList (logical index -> physical index,
* -1 = time column).
*
* Multiple logical columns may map to the same physical column (the server
* deduplicates identical output columns). In practice duplicates share one
* type; if they ever disagree, the physical column's type is left undefined
* so no type-specific conversion is applied.
*
* When the mapping is absent, the logical order is the physical order
* (identity), preserving the previous behavior.
*/
private derivePhysicalColumnTypes(
dataTypes: string[],
columnIndex2TsBlockColumnIndexList?: number[],
): (string | undefined)[] {
if (
!columnIndex2TsBlockColumnIndexList ||
columnIndex2TsBlockColumnIndexList.length === 0
) {
return dataTypes;
}

const physicalTypes: (string | undefined)[] = [];
const conflicting = new Set<number>();
for (let i = 0; i < dataTypes.length; i++) {
const physicalIndex = columnIndex2TsBlockColumnIndexList[i];
// -1 marks the time column; ignore missing/negative entries
if (physicalIndex === undefined || physicalIndex < 0) {
continue;
}
const existing = physicalTypes[physicalIndex];
if (existing === undefined) {
physicalTypes[physicalIndex] = dataTypes[i];
} else if (existing !== dataTypes[i]) {
conflicting.add(physicalIndex);
}
}
for (const physicalIndex of conflicting) {
logger.warn(
`derivePhysicalColumnTypes: conflicting logical types for TsBlock column ${physicalIndex}; skipping type conversion for it`,
);
physicalTypes[physicalIndex] = undefined;
}
return physicalTypes;
}

/**
* Parse a single TsBlock buffer
* TsBlock format (from Apache IoTDB C# client):
Expand All @@ -947,10 +1003,14 @@ export class Session {
* regardless of the ignoreTimeStamp setting. The ignoreTimeStamp flag only
* affects whether the timestamp is included in the returned row data, not the
* TsBlock binary format. This matches the behavior of iotdb-client-csharp.
*
* @param physicalColumnTypes - Logical data types ordered by PHYSICAL
* TsBlock column (see derivePhysicalColumnTypes); an undefined entry means
* "unknown, apply no type-specific conversion"
*/
private parseTsBlock(
buffer: Buffer,
dataTypes: string[],
physicalColumnTypes: (string | undefined)[],
ignoreTimeStamp: boolean,
): any[][] {
let offset = 0;
Expand Down Expand Up @@ -1021,6 +1081,25 @@ export class Session {
);
}

// DATE columns arrive in TsBlock with wire type INT32 (1); the real DATE
// type is only present in the query metadata. Convert those columns'
// yyyyMMdd integers (e.g. 20240101) to Date objects here. Only convert
// when the physical column's logical type is unambiguously DATE.
for (let i = 0; i < valueColumns.length; i++) {
if (
valueColumnTypes[i] === 1 &&
physicalColumnTypes[i] !== undefined &&
this.getDataTypeCode(physicalColumnTypes[i]) === 9
) {
const colValues = valueColumns[i].values;
for (let j = 0; j < colValues.length; j++) {
if (colValues[j] !== null) {
colValues[j] = parseIntToDate(colValues[j]);
}
}
}
}

// Build rows from columns
const rows: any[][] = [];
for (let rowIndex = 0; rowIndex < positionCount; rowIndex++) {
Expand Down Expand Up @@ -1313,15 +1392,14 @@ export class Session {
break;
}
case 9: {
// DATE (stored as INT32 - days since epoch) - TSQueryDataSet uses BIG ENDIAN
// DATE (stored as INT32 - yyyyMMdd) - TSQueryDataSet uses BIG ENDIAN
for (let i = 0; i < rowCount; i++) {
if (this.isNull(bitmap, i)) {
values.push(null);
} else {
// Convert days since epoch to Date object
const days = buffer.readInt32BE(i * 4);
const date = new Date(days * 24 * 60 * 60 * 1000);
values.push(date);
// Convert yyyyMMdd integer to Date object
const encoded = buffer.readInt32BE(i * 4);
values.push(parseIntToDate(encoded));
}
}
break;
Expand Down
1 change: 1 addition & 0 deletions src/client/SessionDataSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export class SessionDataSet {
this.columnNames.length,
this.columnTypes,
this.ignoreTimeStamp,
this.columnIndex2TsBlockColumnIndexList,
);
} else if (response.queryDataSet) {
// Old columnar format (TSQueryDataSet)
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ export {
parseNodeUrls,
} from "./utils/Config";
export { logger, LogLevel } from "./utils/Logger";
export { TSDataType, getDataTypeName } from "./utils/DataTypes";
export {
TSDataType,
getDataTypeName,
parseDateToInt,
parseIntToDate,
} from "./utils/DataTypes";
export { RedirectException, TSStatusCode } from "./utils/Errors";
export { RedirectCache } from "./client/RedirectCache";
export { enableGlobalCleanup } from "./utils/ProcessCleanup";
Expand Down
Loading
Loading