From 34534e08c7b24f12f49e3f93cb662c0f094c0bba Mon Sep 17 00:00:00 2001 From: Prathamesh Baviskar Date: Wed, 26 Aug 2026 18:22:44 +0000 Subject: [PATCH] Avoid false unsupported-operation telemetry for nested timestamps Signed-off-by: Prathamesh Baviskar --- .../jdbc/api/impl/ComplexDataTypeParser.java | 24 +++++------ .../api/impl/ComplexDataTypeParserTest.java | 42 ++++++++++++++++++- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java index cbdd9ddf6..e17ad4121 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java +++ b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java @@ -233,22 +233,18 @@ private Object convertPrimitive(String text, String type) { case DatabricksTypeUtil.TIMESTAMP: case DatabricksTypeUtil.TIMESTAMP_NTZ: try { - return parseTimestamp(text); - } catch (IllegalArgumentException e) { + // Parse Arrow's expected numeric format before TimestampConverter logs a failed attempt. // Arrow serializes TIMESTAMP/TIMESTAMP_NTZ inside nested types as epoch microseconds. // e.g., {"ts":1696519230000000} for 2023-10-05 15:20:30 UTC - try { - long micros = Long.parseLong(text); - long seconds = Math.floorDiv(micros, 1_000_000L); - long microsRemainder = Math.floorMod(micros, 1_000_000L); - Instant instant = Instant.ofEpochSecond(seconds, microsRemainder * 1_000); - // Build from the UTC wall-clock; Timestamp.from(instant) gets re-rendered in the JVM - // default timezone, shifting nested TIMESTAMP fields (ES-1978662). - return Timestamp.valueOf(LocalDateTime.ofInstant(instant, ZoneOffset.UTC)); - } catch (NumberFormatException nfe) { - LOGGER.error(e, "Failed to parse TIMESTAMP value '{}' as epoch microseconds", text); - throw e; - } + long micros = Long.parseLong(text); + long seconds = Math.floorDiv(micros, 1_000_000L); + long microsRemainder = Math.floorMod(micros, 1_000_000L); + Instant instant = Instant.ofEpochSecond(seconds, microsRemainder * 1_000); + // Build from the UTC wall-clock; Timestamp.from(instant) gets re-rendered in the JVM + // default timezone, shifting nested TIMESTAMP fields (ES-1978662). + return Timestamp.valueOf(LocalDateTime.ofInstant(instant, ZoneOffset.UTC)); + } catch (NumberFormatException e) { + return parseTimestamp(text); } case DatabricksTypeUtil.TIME: return Time.valueOf(text); diff --git a/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java b/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java index d17498d88..44ad715fe 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java @@ -1,13 +1,24 @@ package com.databricks.jdbc.api.impl; import static org.junit.jupiter.api.Assertions.*; - +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +import com.databricks.jdbc.api.internal.IDatabricksConnectionContext; +import com.databricks.jdbc.common.DatabricksClientType; +import com.databricks.jdbc.common.TelemetryLogLevel; +import com.databricks.jdbc.common.util.DatabricksThreadContextHolder; import com.databricks.jdbc.exception.DatabricksParsingException; +import com.databricks.jdbc.telemetry.ITelemetryClient; +import com.databricks.jdbc.telemetry.TelemetryClientFactory; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; public class ComplexDataTypeParserTest { @@ -257,6 +268,35 @@ void testTimestampAsEpochMicrosInStruct() throws DatabricksParsingException { } } + @Test + void testTimestampAsEpochMicrosDoesNotEmitFailureTelemetry() throws Exception { + String json = "{\"before_epoch\":-1,\"fractional\":1696519230123456}"; + IDatabricksConnectionContext connectionContext = mock(IDatabricksConnectionContext.class); + when(connectionContext.getTelemetryLogLevel()).thenReturn(TelemetryLogLevel.DEBUG); + when(connectionContext.getConnectionUuid()).thenReturn("epoch-micros-test"); + when(connectionContext.getClientType()).thenReturn(DatabricksClientType.THRIFT); + DatabricksThreadContextHolder.setConnectionContext(connectionContext); + + TelemetryClientFactory factory = mock(TelemetryClientFactory.class); + ITelemetryClient client = mock(ITelemetryClient.class); + + try (MockedStatic telemetryFactory = + Mockito.mockStatic(TelemetryClientFactory.class)) { + telemetryFactory.when(TelemetryClientFactory::getInstance).thenReturn(factory); + when(factory.getTelemetryClient(connectionContext)).thenReturn(client); + DatabricksStruct dbStruct = + parser.parseJsonStringToDbStruct( + json, "STRUCT"); + Object[] attrs = dbStruct.getAttributes(); + + assertEquals(Timestamp.valueOf("1969-12-31 23:59:59.999999"), attrs[0]); + assertEquals(Timestamp.valueOf("2023-10-05 15:20:30.123456"), attrs[1]); + telemetryFactory.verify(TelemetryClientFactory::getInstance, never()); + } finally { + DatabricksThreadContextHolder.clearAllContext(); + } + } + @Test void testTimestampAsEpochMicrosInArray() throws DatabricksParsingException { // TIMESTAMP inside plain ARRAY — Arrow serializes as epoch microseconds