mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 08:18:52 +00:00
fix(tdengine): display timezone timestamps and tags correctly
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user