fix(tdengine): display timezone timestamps and tags correctly

This commit is contained in:
zipg
2026-08-22 19:17:05 +08:00
committed by GitHub
parent 2fee435b33
commit 36c9500a4e
9 changed files with 177 additions and 40 deletions
+39 -7
View File
@@ -28,6 +28,7 @@ pub struct TdengineDriver {
connection: Option<Taos>,
params: ConnectParams,
server_version: Option<String>,
session_timezone: Option<Tz>,
current_database: String,
query_sessions: HashMap<String, QueryCursor>,
table_cache: Option<TableCache>,
@@ -42,6 +43,7 @@ struct TableCache {
struct QueryCursor {
result_set: ResultSet,
timezone: Option<Tz>,
session_timezone: Option<Tz>,
columns: Vec<String>,
column_types: Vec<String>,
affected_rows: i64,
@@ -59,6 +61,7 @@ impl TdengineDriver {
connection: None,
params: ConnectParams::default(),
server_version: None,
session_timezone: None,
current_database: String::new(),
query_sessions: HashMap::new(),
table_cache: None,
@@ -76,8 +79,13 @@ impl TdengineDriver {
.map_err(|_| anyhow!("TDengine connection timed out after {} seconds", connect_timeout.as_secs()))??;
let token = CancellationToken::new();
let server_version = query_scalar_string(&connection, "SELECT server_version()", &token, 5).await.ok();
let session_timezone = query_scalar_string(&connection, "SELECT timezone()", &token, 5)
.await
.ok()
.and_then(|value| parse_server_timezone(&value));
self.current_database = dsn.database;
self.server_version = server_version;
self.session_timezone = session_timezone;
self.params = params;
self.connection = Some(connection);
Ok(())
@@ -95,6 +103,7 @@ impl TdengineDriver {
self.query_sessions.clear();
self.table_cache = None;
self.server_version = None;
self.session_timezone = None;
self.current_database.clear();
self.connection = None;
}
@@ -469,6 +478,7 @@ impl TdengineDriver {
Ok(QueryCursor {
result_set,
timezone,
session_timezone: self.session_timezone,
columns,
column_types,
affected_rows,
@@ -694,7 +704,7 @@ impl QueryCursor {
loop {
if let Some(block) = &self.pending_block {
if self.pending_row_index < block.nrows() {
let timezone = preferred_timezone(self.timezone, block.timezone());
let timezone = preferred_timezone(self.timezone, block.timezone(), self.session_timezone);
let row_index = self.pending_row_index;
self.pending_row_index += 1;
let row = (0..block.ncols())
@@ -763,15 +773,23 @@ async fn query_scalar_string(
continue;
}
let value = block.get_ref(0, 0).ok_or_else(|| anyhow!("TDengine query returned an empty value"))?;
return match borrowed_value_to_json(value, preferred_timezone(timezone, block.timezone())) {
return match borrowed_value_to_json(value, preferred_timezone(timezone, block.timezone(), None)) {
Value::String(value) => Ok(value),
value => Ok(value.to_string()),
};
}
}
fn preferred_timezone(query_timezone: Option<Tz>, block_timezone: Option<Tz>) -> Option<Tz> {
query_timezone.or(block_timezone)
fn preferred_timezone(
query_timezone: Option<Tz>,
block_timezone: Option<Tz>,
session_timezone: Option<Tz>,
) -> Option<Tz> {
query_timezone.or(block_timezone).or(session_timezone)
}
fn parse_server_timezone(value: &str) -> Option<Tz> {
value.split_whitespace().next()?.parse().ok()
}
async fn cancellable<T, F>(token: &CancellationToken, timeout_secs: u64, future: F) -> Result<T>
@@ -1063,9 +1081,23 @@ mod tests {
#[test]
fn prefers_query_timezone_and_falls_back_to_block_timezone() {
assert_eq!(preferred_timezone(Some(chrono_tz::UTC), Some(chrono_tz::Asia::Shanghai)), Some(chrono_tz::UTC));
assert_eq!(preferred_timezone(None, Some(chrono_tz::Asia::Shanghai)), Some(chrono_tz::Asia::Shanghai));
assert_eq!(preferred_timezone(None, None), None);
assert_eq!(
preferred_timezone(Some(chrono_tz::UTC), Some(chrono_tz::Asia::Tokyo), Some(chrono_tz::Asia::Shanghai)),
Some(chrono_tz::UTC)
);
assert_eq!(
preferred_timezone(None, Some(chrono_tz::Asia::Tokyo), Some(chrono_tz::Asia::Shanghai)),
Some(chrono_tz::Asia::Tokyo)
);
assert_eq!(preferred_timezone(None, None, Some(chrono_tz::Asia::Shanghai)), Some(chrono_tz::Asia::Shanghai));
assert_eq!(preferred_timezone(None, None, None), None);
}
#[test]
fn parses_tdengine_timezone_description() {
assert_eq!(parse_server_timezone("Asia/Shanghai (CST, +0800)"), Some(chrono_tz::Asia::Shanghai));
assert_eq!(parse_server_timezone("Etc/UTC (UTC, +0000)"), Some(chrono_tz::Etc::UTC));
assert_eq!(parse_server_timezone("unknown (+0800)"), None);
}
#[test]
@@ -5577,6 +5577,7 @@ function cellDetailFor(rowIndex: number, columnIndex: number): DataGridCellDetai
canEditCell: canEditCellItem(item, columnIndex),
isDraft: !!item.isDraft,
}),
databaseType: resolvedDatabaseType.value,
isValuePreviewTruncated: isLargeValuePreview(item, columnIndex),
});
}
@@ -5706,7 +5707,7 @@ const activeCellDetailTabs = computed(() => {
const activeBinaryHexBytes = computed(() => {
if (activeCellDetailTab.value !== "hexViewer") return null;
const detail = activeCellDetail.value;
return detail ? parseBinaryCellBytes(detail.value, detail.type) : null;
return detail ? parseBinaryCellBytes(detail.value, detail.type, resolvedDatabaseType.value) : null;
});
const activeBinaryHexRows = computed(() => (activeBinaryHexBytes.value ? buildBinaryHexViewRows(activeBinaryHexBytes.value) : []));
@@ -6326,7 +6327,7 @@ function formatCell(value: CellValue, columnIndex?: number, originalBytes?: numb
columnInfo: displayColumnInfo,
});
if (arrayDisplay !== undefined) return arrayDisplay;
const binaryDisplay = formatter ? null : binaryCellDisplayText(value, columnIndex === undefined ? undefined : allColumnTypes.value[columnIndex], originalBytes);
const binaryDisplay = formatter ? null : binaryCellDisplayText(value, columnIndex === undefined ? undefined : allColumnTypes.value[columnIndex], originalBytes, resolvedDatabaseType.value);
if (binaryDisplay !== null) return binaryDisplay;
const s = applyColumnFormatter(value, formatter);
return limitDisplay ? limitDataGridCellDisplay(s, resolvedDatabaseType.value === "sqlserver" ? SQLSERVER_DATA_GRID_CELL_DISPLAY_MAX_LENGTH : undefined) : s;
@@ -8922,7 +8923,7 @@ function copyDetailColumnName() {
function canDownloadDetailBinaryValue(detail: DataGridCellDetail | null): boolean {
if (!detail) return false;
const item = getRowItem(detail.rowId);
return (isLargeValuePreview(item, detail.colIndex) && isBinaryCellColumnType(detail.type)) || canDownloadBinaryCellValue(detail.value, detail.type);
return (resolvedDatabaseType.value !== "tdengine" && isLargeValuePreview(item, detail.colIndex) && isBinaryCellColumnType(detail.type)) || canDownloadBinaryCellValue(detail.value, detail.type, resolvedDatabaseType.value);
}
function canImportDetailBinaryValue(detail: DataGridCellDetail | null): boolean {
@@ -8972,7 +8973,7 @@ async function downloadDetailBinaryValue(detail: DataGridCellDetail | null, mode
if (!(await hydrateLargeValueCell(detail.rowId, detail.colIndex))) return;
const resolvedDetail = cellDetailFor(detail.rowNumber - 1, detail.colIndex);
if (!resolvedDetail) return;
const payload = binaryCellDownloadPayload(resolvedDetail.value, mode, resolvedDetail.type);
const payload = binaryCellDownloadPayload(resolvedDetail.value, mode, resolvedDetail.type, resolvedDatabaseType.value);
const fileName = binaryCellDownloadFileName({
column: resolvedDetail.column,
rowNumber: resolvedDetail.rowNumber,
@@ -159,7 +159,7 @@ function bytesFromBufferLikeObject(value: unknown): Uint8Array | null {
return bytesFromByteArray(data);
}
export function parseBinaryCellBytes(value: unknown, columnType?: string): Uint8Array | null {
export function parseBinaryCellBytes(value: unknown, columnType?: string, databaseType?: DatabaseType): Uint8Array | null {
if (typeof value === "string") {
const prefixed = parseBinaryCellHexValue(value);
if (prefixed) return prefixed;
@@ -169,7 +169,7 @@ export function parseBinaryCellBytes(value: unknown, columnType?: string): Uint8
return bytesFromHex(trimmed.replace(/\\x/gi, ""));
}
if (isBinaryCellColumnType(columnType) && BARE_HEX_RE.test(trimmed)) {
if (databaseType !== "tdengine" && isBinaryCellColumnType(columnType) && BARE_HEX_RE.test(trimmed)) {
return bytesFromHex(trimmed);
}
}
@@ -189,16 +189,16 @@ export function canImportBinaryCellFile(databaseType?: DatabaseType, columnType?
return false;
}
export function canDownloadBinaryCellValue(value: unknown, columnType?: string): boolean {
return !!parseBinaryCellBytes(value, columnType);
export function canDownloadBinaryCellValue(value: unknown, columnType?: string, databaseType?: DatabaseType): boolean {
return !!parseBinaryCellBytes(value, columnType, databaseType);
}
export function binaryCellDisplayText(value: unknown, columnType?: string, originalBytes?: number): string | null {
export function binaryCellDisplayText(value: unknown, columnType?: string, originalBytes?: number, databaseType?: DatabaseType): string | null {
if (typeof value === "string" && isBinaryCellColumnType(columnType) && TRUNCATED_HEX_VALUE_RE.test(value.trim())) {
const size = originalBytes === undefined ? "..." : formatBinaryCellByteSize(originalBytes);
return `${binaryCellDisplayLabel(columnType)} [${size}]`;
}
const bytes = parseBinaryCellBytes(value, columnType);
const bytes = parseBinaryCellBytes(value, columnType, databaseType);
if (!bytes || !isBinaryCellColumnType(columnType)) return null;
if (BINARY_STRING_TYPE_RE.test((columnType ?? "").trim())) {
const previewBytes = FIXED_BINARY_TYPE_RE.test((columnType ?? "").trim()) ? trimTrailingNullBytes(bytes) : bytes;
@@ -245,8 +245,8 @@ export function formatBinaryCellByteSize(bytes: number): string {
return `${(bytes / (1024 * 1024)).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
}
export function binaryCellDownloadPayload(value: unknown, mode: BinaryCellDownloadMode, columnType?: string): BinaryCellDownloadPayload {
const bytes = parseBinaryCellBytes(value, columnType);
export function binaryCellDownloadPayload(value: unknown, mode: BinaryCellDownloadMode, columnType?: string, databaseType?: DatabaseType): BinaryCellDownloadPayload {
const bytes = parseBinaryCellBytes(value, columnType, databaseType);
if (!bytes) {
throw new Error("Cell value is not a downloadable binary value.");
}
+10 -6
View File
@@ -1,5 +1,6 @@
import type { CellValue } from "@/lib/dataGrid/cellValue";
import { isBinaryCellColumnType, parseBinaryCellBytes } from "@/lib/dataGrid/binaryCellDownload";
import type { DatabaseType } from "@/types/database";
const IMAGE_PATH_RE = /\.(?:png|jpe?g|webp|gif|avif|bmp|svg)$/i;
const SAFE_DATA_IMAGE_RE = /^data:image\/(?:png|jpe?g|webp|gif|avif|bmp);base64,[a-z0-9+/=\s]+$/i;
@@ -14,10 +15,11 @@ function isLocalHttpHost(hostname: string): boolean {
export interface CellImagePreviewOptions {
binary?: boolean;
databaseType?: DatabaseType;
}
export function cellImagePreviewUrl(value: CellValue | unknown, columnType?: string, options: CellImagePreviewOptions = {}): string | null {
const binaryPreviewUrl = options.binary === false ? null : binaryImagePreviewUrl(value, columnType);
const binaryPreviewUrl = options.binary === false ? null : binaryImagePreviewUrl(value, columnType, options.databaseType);
if (binaryPreviewUrl) return binaryPreviewUrl;
if (typeof value !== "string") return null;
@@ -38,16 +40,16 @@ export function cellImagePreviewUrl(value: CellValue | unknown, columnType?: str
return text;
}
function binaryImagePreviewUrl(value: unknown, columnType?: string): string | null {
if (estimatedBinaryByteLength(value, columnType) > MAX_BINARY_IMAGE_PREVIEW_BYTES) return null;
const bytes = parseBinaryCellBytes(value, columnType);
function binaryImagePreviewUrl(value: unknown, columnType?: string, databaseType?: DatabaseType): string | null {
if (estimatedBinaryByteLength(value, columnType, databaseType) > MAX_BINARY_IMAGE_PREVIEW_BYTES) return null;
const bytes = parseBinaryCellBytes(value, columnType, databaseType);
if (!bytes || bytes.length > MAX_BINARY_IMAGE_PREVIEW_BYTES) return null;
const mimeType = detectImageMimeType(bytes);
if (!mimeType) return null;
return `data:${mimeType};base64,${bytesToBase64(bytes)}`;
}
function estimatedBinaryByteLength(value: unknown, columnType?: string): number {
function estimatedBinaryByteLength(value: unknown, columnType?: string, databaseType?: DatabaseType): number {
if (Array.isArray(value)) return value.length;
if (value && typeof value === "object" && Array.isArray((value as { data?: unknown }).data)) {
return (value as { data: unknown[] }).data.length;
@@ -58,7 +60,9 @@ function estimatedBinaryByteLength(value: unknown, columnType?: string): number
const prefixed = trimmed.match(HEX_PREFIX_RE);
if (prefixed) return prefixed[1].replace(/\s+/g, "").length / 2;
if (HEX_ESCAPE_RE.test(trimmed)) return trimmed.replace(/\s+/g, "").replace(/\\x/gi, "").length / 2;
if (isBinaryCellColumnType(columnType) && BARE_HEX_RE.test(trimmed)) return trimmed.replace(/\s+/g, "").length / 2;
if (databaseType !== "tdengine" && isBinaryCellColumnType(columnType) && BARE_HEX_RE.test(trimmed)) {
return trimmed.replace(/\s+/g, "").length / 2;
}
return 0;
}
@@ -1,6 +1,7 @@
import { cellImagePreviewUrl } from "@/lib/dataGrid/cellImageUrl";
import { displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
import { formatJsonText } from "@/lib/dataGrid/cellDetailPresentation";
import type { DatabaseType } from "@/types/database";
export const CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH = 12_000;
@@ -48,6 +49,7 @@ export interface BuildDataGridCellDetailOptions {
commentByColumn?: ReadonlyMap<string, string>;
displayValue: (value: CellValue, columnIndex: number) => string;
isEditable: boolean;
databaseType?: DatabaseType;
includeBinaryImagePreview?: boolean;
isValuePreviewTruncated?: boolean;
}
@@ -109,7 +111,10 @@ export function buildDataGridCellDetail(options: BuildDataGridCellDetailOptions)
displayValue,
displayValuePreview,
isValuePreviewTruncated: options.isValuePreviewTruncated === true || rawValuePreview.length < rawValue.length || displayValuePreview.length < displayValue.length,
imagePreviewUrl: cellImagePreviewUrl(value, type, { binary: options.includeBinaryImagePreview !== false }),
imagePreviewUrl: cellImagePreviewUrl(value, type, {
binary: options.includeBinaryImagePreview !== false,
databaseType: options.databaseType,
}),
length: value === null ? 0 : String(value).length,
formattedJson,
isEditable: options.isEditable,
+11 -1
View File
@@ -37,6 +37,16 @@ test("parseBinaryCellBytes accepts common driver binary shapes", () => {
assert.deepEqual(Array.from(parseBinaryCellBytes({ type: "Buffer", data: [222, 173, 190, 239] }) ?? []), [222, 173, 190, 239]);
});
test("TDengine BINARY text is not treated as unprefixed hex", () => {
for (const value of ["66", "67", "81", "97"]) {
assert.equal(parseBinaryCellBytes(value, "BINARY(16)", "tdengine"), null);
assert.equal(binaryCellDisplayText(value, "BINARY(16)", undefined, "tdengine"), null);
assert.equal(canDownloadBinaryCellValue(value, "BINARY(16)", "tdengine"), false);
}
assert.deepEqual(Array.from(parseBinaryCellBytes("0x3636", "BINARY(16)", "tdengine") ?? []), [54, 54]);
assert.equal(binaryCellDisplayText("0x3636", "BINARY(16)", undefined, "tdengine"), "66");
});
test("binary cell download detects common blob column types", () => {
assert.equal(isBinaryCellColumnType("BLOB"), true);
assert.equal(isBinaryCellColumnType("RAW(2000)"), true);
@@ -104,7 +114,7 @@ test("DataGrid binary preview prefers ResultSet column types when table metadata
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
const formatter = source.match(/function formatCell\([^]*?\n\}/)?.[0] ?? "";
assert.match(formatter, /binaryCellDisplayText\(value, columnIndex === undefined \? undefined : allColumnTypes\.value\[columnIndex\], originalBytes\)/);
assert.match(formatter, /binaryCellDisplayText\(value, columnIndex === undefined \? undefined : allColumnTypes\.value\[columnIndex\], originalBytes, resolvedDatabaseType\.value\)/);
});
test("binaryCellDownloadPayload decodes GBK text bytes", () => {
+5
View File
@@ -38,6 +38,11 @@ test("detects binary image values from hex strings", () => {
assert.equal(cellImagePreviewUrl("89504e470d0a1a0a"), null);
});
test("does not treat TDengine BINARY text as unprefixed image hex", () => {
assert.equal(cellImagePreviewUrl("89504e470d0a1a0a", "BINARY(16)", { databaseType: "tdengine" }), null);
assert.match(cellImagePreviewUrl("0x89504e470d0a1a0a", "BINARY(16)", { databaseType: "tdengine" }) ?? "", /^data:image\/png;base64,/);
});
test("can skip generated binary image previews while keeping URL previews", () => {
assert.equal(cellImagePreviewUrl("0x89504e470d0a1a0a0000000d49484452", "longblob", { binary: false }), null);
assert.equal(cellImagePreviewUrl("https://cdn.example.com/avatar.png", "longblob", { binary: false }), "https://cdn.example.com/avatar.png");
@@ -37,6 +37,8 @@ import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
@@ -732,6 +734,37 @@ public final class DbxJdbcPlugin {
}
}
private static ZoneId tdengineTimestampZone(JsonNode connection, Connection jdbcConnection) {
String url = jdbcUrl(connection);
if (
!urlMatchesPrefix(url, "jdbc:taos:") &&
!urlMatchesPrefix(url, "jdbc:taos-ws:") &&
!urlMatchesPrefix(url, "jdbc:taos-rs:")
) {
return null;
}
try (
Statement statement = jdbcConnection.createStatement();
ResultSet result = statement.executeQuery("SELECT timezone()")
) {
return result.next() ? parseTdengineTimezone(result.getString(1)) : null;
} catch (SQLException | AbstractMethodError | UnsupportedOperationException ignored) {
return null;
}
}
static ZoneId parseTdengineTimezone(String value) {
if (value == null || value.isBlank()) {
return null;
}
String name = value.trim().split("\\s+", 2)[0];
try {
return ZoneId.of(name);
} catch (RuntimeException ignored) {
return null;
}
}
private static JsonNode executeQuery(
JsonNode connection,
String sql,
@@ -747,6 +780,7 @@ public final class DbxJdbcPlugin {
applyExecutionContext(connection, conn, database, schema);
JdbcDriverQuirks quirks = driverQuirks(connection);
boolean preserveOracleDateTime = isOracleUrl(jdbcUrl(connection));
ZoneId timestampZone = tdengineTimestampZone(connection, conn);
try (Statement statement = conn.createStatement()) {
applyStatementOptions(statement, maxRows, fetchSize, timeoutSecs, quirks);
String trimmedSql = trimStatementSql(sql);
@@ -776,7 +810,7 @@ public final class DbxJdbcPlugin {
}
ArrayNode row = MAPPER.createArrayNode();
for (int i = 1; i <= columnCount; i++) {
row.add(MAPPER.valueToTree(readValue(rs, meta, i, preserveOracleDateTime)));
row.add(MAPPER.valueToTree(readValue(rs, meta, i, preserveOracleDateTime, timestampZone)));
}
rows.add(row);
}
@@ -806,6 +840,7 @@ public final class DbxJdbcPlugin {
private final Connection connection;
private final boolean restoreAutoCommit;
private final boolean preserveOracleDateTime;
private final ZoneId timestampZone;
private int rowsReturned;
private ArrayNode pendingRow;
@@ -819,7 +854,8 @@ public final class DbxJdbcPlugin {
long startNanos,
Connection connection,
boolean restoreAutoCommit,
boolean preserveOracleDateTime
boolean preserveOracleDateTime,
ZoneId timestampZone
) {
this.id = id;
this.statement = statement;
@@ -831,6 +867,7 @@ public final class DbxJdbcPlugin {
this.connection = connection;
this.restoreAutoCommit = restoreAutoCommit;
this.preserveOracleDateTime = preserveOracleDateTime;
this.timestampZone = timestampZone;
}
}
@@ -849,6 +886,7 @@ public final class DbxJdbcPlugin {
applyExecutionContext(connection, conn, database, schema);
JdbcDriverQuirks quirks = driverQuirks(connection);
boolean preserveOracleDateTime = isOracleUrl(jdbcUrl(connection));
ZoneId timestampZone = tdengineTimestampZone(connection, conn);
boolean restoreAutoCommit = beginPagedQueryTransaction(connection, conn);
Statement statement;
try {
@@ -895,7 +933,8 @@ public final class DbxJdbcPlugin {
start,
conn,
restoreAutoCommit,
preserveOracleDateTime
preserveOracleDateTime,
timestampZone
);
QUERY_SESSIONS.put(sessionId, session);
try {
@@ -968,7 +1007,7 @@ public final class DbxJdbcPlugin {
closeQuerySession(session.id);
return queryPageResult(session, rows, false, false);
}
row = readRow(session.resultSet, session.meta, session.preserveOracleDateTime);
row = readRow(session.resultSet, session.meta, session.preserveOracleDateTime, session.timestampZone);
}
rows.add(row);
session.rowsReturned++;
@@ -986,7 +1025,7 @@ public final class DbxJdbcPlugin {
return queryPageResult(session, rows, false, false);
}
session.pendingRow = readRow(session.resultSet, session.meta, session.preserveOracleDateTime);
session.pendingRow = readRow(session.resultSet, session.meta, session.preserveOracleDateTime, session.timestampZone);
return queryPageResult(session, rows, false, true);
}
@@ -1039,11 +1078,12 @@ public final class DbxJdbcPlugin {
private static ArrayNode readRow(
ResultSet rs,
ResultSetMetaData meta,
boolean preserveOracleDateTime
boolean preserveOracleDateTime,
ZoneId timestampZone
) throws SQLException {
ArrayNode row = MAPPER.createArrayNode();
for (int i = 1; i <= meta.getColumnCount(); i++) {
row.add(MAPPER.valueToTree(readValue(rs, meta, i, preserveOracleDateTime)));
row.add(MAPPER.valueToTree(readValue(rs, meta, i, preserveOracleDateTime, timestampZone)));
}
return row;
}
@@ -3261,6 +3301,16 @@ public final class DbxJdbcPlugin {
ResultSetMetaData meta,
int index,
boolean preserveOracleDateTime
) throws SQLException {
return readValue(rs, meta, index, preserveOracleDateTime, null);
}
private static Object readValue(
ResultSet rs,
ResultSetMetaData meta,
int index,
boolean preserveOracleDateTime,
ZoneId timestampZone
) throws SQLException {
int columnType = meta.getColumnType(index);
@@ -3297,11 +3347,14 @@ public final class DbxJdbcPlugin {
byte[] bytes = rs.getBytes(index);
return bytes == null ? null : binaryToHex(bytes);
}
Object temporalValue = readTemporalValue(rs, meta, index, preserveOracleDateTime);
Object temporalValue = readTemporalValue(rs, meta, index, preserveOracleDateTime, timestampZone);
if (temporalValue != null) {
return temporalValue;
}
if (value instanceof Date || value instanceof Time || value instanceof Timestamp || value instanceof TemporalAccessor) {
if (value instanceof Timestamp timestamp) {
return formatTimestamp(timestamp, timestampZone);
}
if (value instanceof Date || value instanceof Time || value instanceof TemporalAccessor) {
return value.toString();
}
if (value instanceof BigDecimal decimal) {
@@ -3331,7 +3384,8 @@ public final class DbxJdbcPlugin {
ResultSet rs,
ResultSetMetaData meta,
int index,
boolean preserveOracleDateTime
boolean preserveOracleDateTime,
ZoneId timestampZone
) throws SQLException {
return switch (meta.getColumnType(index)) {
case Types.DATE -> {
@@ -3348,12 +3402,20 @@ public final class DbxJdbcPlugin {
}
case Types.TIMESTAMP -> {
Timestamp timestamp = rs.getTimestamp(index);
yield timestamp == null ? null : timestamp.toString();
yield timestamp == null ? null : formatTimestamp(timestamp, timestampZone);
}
default -> null;
};
}
static String formatTimestamp(Timestamp timestamp, ZoneId timestampZone) {
if (timestampZone == null) {
return timestamp.toString();
}
LocalDateTime local = LocalDateTime.ofInstant(timestamp.toInstant(), timestampZone);
return Timestamp.valueOf(local).toString();
}
private static boolean isBinaryColumn(ResultSetMetaData meta, int index) throws SQLException {
return switch (meta.getColumnType(index)) {
case Types.BINARY,
@@ -27,6 +27,8 @@ import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
@@ -55,6 +57,20 @@ final class DbxJdbcPluginTest {
""".formatted(CONNECTION));
}
@Test
void parsesTdengineTimezoneDescription() {
assertEquals(ZoneId.of("Asia/Shanghai"), DbxJdbcPlugin.parseTdengineTimezone("Asia/Shanghai (CST, +0800)"));
assertEquals(ZoneId.of("Etc/UTC"), DbxJdbcPlugin.parseTdengineTimezone("Etc/UTC (UTC, +0000)"));
assertEquals(null, DbxJdbcPlugin.parseTdengineTimezone("unknown (+0800)"));
}
@Test
void formatsTimestampInTdengineSessionTimezone() {
Timestamp timestamp = Timestamp.from(Instant.parse("2025-03-06T01:19:49.433Z"));
assertEquals("2025-03-06 09:19:49.433", DbxJdbcPlugin.formatTimestamp(timestamp, ZoneId.of("Asia/Shanghai")));
}
@Test
void executeQueryAppliesSchemaContext() throws Exception {
request("executeQuery", """
@@ -690,7 +706,7 @@ final class DbxJdbcPluginTest {
assertFalse(response.has("error"), response.toString());
assertEquals("row-value", response.path("result").path("rows").path(0).path(0).asText());
assertEquals(List.of("createStatement", "executeQuery"), calls);
assertEquals(List.of("createStatement", "executeQuery", "createStatement", "executeQuery"), calls);
} finally {
DriverManager.deregisterDriver(driver);
}
@@ -728,6 +744,8 @@ final class DbxJdbcPluginTest {
"setCatalog:bopu_light",
"setClientInfo:dbname:bopu_light",
"createStatement",
"executeQuery",
"createStatement",
"executeQuery"
),
calls,
@@ -787,7 +805,7 @@ final class DbxJdbcPluginTest {
""".formatted(connection));
assertFalse(response.has("error"), response.toString());
assertEquals(List.of("createStatement", "executeQuery"), calls);
assertEquals(List.of("createStatement", "executeQuery", "createStatement", "executeQuery"), calls);
} finally {
closeAndDeregister(connection, driver);
}