fix(iotdb): render timestamps in session zone and type max_by

This commit is contained in:
zipg
2026-09-03 01:12:47 +08:00
committed by GitHub
parent 45688cafd6
commit 6848e78855
6 changed files with 62 additions and 6 deletions
+31
View File
@@ -133,6 +133,37 @@ func TestLiveIoTDBAgentTreeDatabaseConnection(t *testing.T) {
}
}
func TestLiveIoTDBAgentTreeTimeAggregates(t *testing.T) {
if os.Getenv("DBX_IOTDB_LIVE") != "1" {
t.Skip("set DBX_IOTDB_LIVE=1 to run against a real IoTDB server")
}
database := "root.dbx_go_time_" + strconv.Itoa(os.Getpid())
device := database + ".d1"
server := liveIoTDBServer(t, connectParams{Database: database})
defer server.disconnect()
defer func() { _ = server.executeNonQuery("DELETE DATABASE "+database, "", 0) }()
mustExecuteNonQuery(t, server, "CREATE DATABASE "+database, "")
mustExecuteNonQuery(t, server, "CREATE TIMESERIES "+device+".s1 WITH DATATYPE=INT64, ENCODING=RLE", "")
mustExecuteNonQuery(t, server, "INSERT INTO "+device+"(time,s1) VALUES(1,10),(2,20)", "")
result, err := server.executeQuery(queryOptions{
SQL: "SELECT max_time(s1), max_by(time,s1), max_by(s1,time) FROM " + device,
MaxRows: 10,
})
if err != nil || len(result.Rows) != 1 {
t.Fatalf("tree time aggregates = %#v, %v", result, err)
}
wantTypes := []string{"TIMESTAMP(ms)", "TIMESTAMP(ms)", "INT64"}
if !reflect.DeepEqual(result.ColumnTypes, wantTypes) {
t.Fatalf("tree time aggregate types = %#v, want %#v", result.ColumnTypes, wantTypes)
}
wantRow := []any{"2", "2", int64(20)}
if !reflect.DeepEqual(result.Rows[0], wantRow) {
t.Fatalf("tree time aggregate row = %#v, want %#v", result.Rows[0], wantRow)
}
}
func TestLiveIoTDBAgentProcessMultiSessionAndCancellation(t *testing.T) {
if os.Getenv("DBX_IOTDB_LIVE") != "1" {
t.Skip("set DBX_IOTDB_LIVE=1 to run against a real IoTDB server")
+9 -2
View File
@@ -183,11 +183,13 @@ func TestTreeTimeAggregationColumnsPresentAsTimestamps(t *testing.T) {
"max_time(root.db.d1.s1)",
"MIN_TIME(root.db.d1.s1)",
" max_time(root.db.d1.s1) ",
"max_by(Time,root.db.d1.s1)",
"MIN_BY( time , root.db.d1.s1 )",
"avg(root.db.d1.s1)",
"count(root.db.d1.s1)",
}
got := normalizedColumnTypes([]string{"INT64", "INT64", "INT64", "DOUBLE", "INT64"}, columns, client.TreeSqlDialect, "ms")
want := []string{"TIMESTAMP(ms)", "TIMESTAMP(ms)", "TIMESTAMP(ms)", "DOUBLE", "INT64"}
got := normalizedColumnTypes([]string{"INT64", "INT64", "INT64", "INT64", "INT64", "DOUBLE", "INT64"}, columns, client.TreeSqlDialect, "ms")
want := []string{"TIMESTAMP(ms)", "TIMESTAMP(ms)", "TIMESTAMP(ms)", "TIMESTAMP(ms)", "TIMESTAMP(ms)", "DOUBLE", "INT64"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected tree aggregation types: %#v", got)
}
@@ -197,6 +199,11 @@ func TestTreeTimeAggregationColumnsPresentAsTimestamps(t *testing.T) {
if table := normalizedColumnTypes([]string{"INT64"}, []string{"max_time(t1.s1)"}, client.TableSqlDialect, "ms"); !reflect.DeepEqual(table, []string{"INT64"}) {
t.Fatalf("table dialect must stay INT64: %#v", table)
}
for _, column := range []string{"max_by(s1,time)", "max_by(event_time,s1)", "max_bytes(time,s1)"} {
if got := normalizedColumnTypes([]string{"INT64"}, []string{column}, client.TreeSqlDialect, "ms"); !reflect.DeepEqual(got, []string{"INT64"}) {
t.Fatalf("non-time max_by result %q must stay INT64: %#v", column, got)
}
}
}
func TestClassifyIoTDBErrors(t *testing.T) {
+10
View File
@@ -493,6 +493,16 @@ func isTimeAggregationColumn(column string) bool {
return true
}
}
for _, prefix := range []string{"max_by(", "min_by("} {
if len(trimmed) <= len(prefix) || !strings.EqualFold(trimmed[:len(prefix)], prefix) {
continue
}
arguments := trimmed[len(prefix):]
comma := strings.IndexByte(arguments, ',')
if comma > 0 && strings.EqualFold(strings.TrimSpace(arguments[:comma]), "time") {
return true
}
}
return false
}
@@ -164,9 +164,14 @@ describe("defaultIoTDBTimestampFormatter", () => {
expect(defaultIoTDBTimestampFormatter("iotdb", "INT64", "time_zone=Asia%2FShanghai")).toBeUndefined();
});
it("uses UTC when the connection does not specify a time zone", () => {
it("uses the IoTDB client session default when the connection does not specify a time zone", () => {
const formatter = defaultIoTDBTimestampFormatter("iotdb", "TIMESTAMP(ms)", "");
expect(applyColumnFormatter(1, formatter)).toBe("1970-01-01T00:00:00.001+00:00");
expect(applyColumnFormatter("1787759999000", formatter)).toBe("2026-08-26T23:59:59.000+08:00");
});
it("falls back to the IoTDB client session default for an invalid configured zone", () => {
const formatter = defaultIoTDBTimestampFormatter("iotdb", "TIMESTAMP(ms)", "time_zone=Not%2FAZone");
expect(applyColumnFormatter(1, formatter)).toBe("1970-01-01T08:00:00.001+08:00");
});
it("round-trips a negative nanosecond timestamp without truncating toward zero", () => {
@@ -429,12 +429,15 @@ export function iotdbTimestampFractionDigits(precision: IoTDBTimestampPrecision)
function iotdbTimestampTimeZone(urlParams: string | undefined): string {
const params = new URLSearchParams(urlParams ?? "");
const candidate = params.get("time_zone") || params.get("timezone") || params.get("zone_id") || "UTC";
// Keep display aligned with the IoTDB Go client's default session zone.
// Otherwise an unconfigured connection interprets SQL literals in
// Asia/Shanghai but the grid renders the returned epoch in UTC.
const candidate = params.get("time_zone") || params.get("timezone") || params.get("zone_id") || "Asia/Shanghai";
try {
dayjs(0).tz(candidate);
return candidate;
} catch {
return "UTC";
return "Asia/Shanghai";
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB