Skip to content

Commit 5a3d1f7

Browse files
test: add parameter matrix and timestamp examples, fix patch coverage
- Add parameter-setting matrix (12 setter × cast combinations + null cases) into TimeZoneIntegrationTest — covers setTimestamp, setObject with Instant/LocalDateTime/OffsetDateTime/ZonedDateTime/Timestamp, and explicit TIMESTAMP/TIMESTAMP_WITH_TIMEZONE type hints - Add TimestampUsageTest in examples package: LocalDateTime (wall-clock TIMESTAMP), OffsetDateTime (UTC instant TIMESTAMPTZ), legacy Timestamp, and session timezone behavior - Remove dead calendar branches from TimeStampVectorAccessor.getOffsetDateTime() and getZonedDateTime() — always called with null from public API - Add targeted unit tests for previously uncovered patch lines: getObjectClass(), TZ systemDefault fallback, null querySettings path
1 parent a062d30 commit 5a3d1f7

5 files changed

Lines changed: 488 additions & 15 deletions

File tree

jdbc-core/src/main/java/com/salesforce/datacloud/jdbc/core/accessor/impl/TimeStampVectorAccessor.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,11 @@ Instant getInstant() {
9595
}
9696
}
9797

98-
private OffsetDateTime getOffsetDateTime(Calendar calendar) {
98+
private OffsetDateTime getOffsetDateTime() {
9999
Instant instant = getInstant();
100100
if (instant == null) {
101101
return null;
102102
}
103-
104-
if (calendar != null) {
105-
return OffsetDateTime.ofInstant(instant, calendar.getTimeZone().toZoneId());
106-
}
107-
108103
return OffsetDateTime.ofInstant(instant, UTC);
109104
}
110105

@@ -116,16 +111,11 @@ private LocalDateTime getLocalDateTime(Calendar calendar) {
116111
return LocalDateTime.ofInstant(instant, UTC);
117112
}
118113

119-
private ZonedDateTime getZonedDateTime(Calendar calendar) {
114+
private ZonedDateTime getZonedDateTime() {
120115
Instant instant = getInstant();
121116
if (instant == null) {
122117
return null;
123118
}
124-
125-
if (calendar != null) {
126-
return ZonedDateTime.ofInstant(instant, calendar.getTimeZone().toZoneId());
127-
}
128-
129119
return ZonedDateTime.ofInstant(instant, UTC);
130120
}
131121

@@ -188,10 +178,10 @@ public <T> T getObject(Class<T> type) throws SQLException {
188178
return (T) getInstant();
189179
}
190180
if (type == OffsetDateTime.class) {
191-
return (T) getOffsetDateTime(null);
181+
return (T) getOffsetDateTime();
192182
}
193183
if (type == ZonedDateTime.class) {
194-
return (T) getZonedDateTime(null);
184+
return (T) getZonedDateTime();
195185
}
196186
if (type == LocalDateTime.class) {
197187
return (T) getLocalDateTime(null);

jdbc-core/src/test/java/com/salesforce/datacloud/jdbc/core/DataCloudStatementTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,14 @@ public void testResolveSessionTimeZoneWithNoTimezoneSetting() {
102102
ZoneId result = statement.resolveSessionTimeZone();
103103
assertThat(result).isEqualTo(ZoneId.systemDefault());
104104
}
105+
106+
@Test
107+
@SneakyThrows
108+
public void testResolveSessionTimeZoneWithNullQuerySettings() {
109+
statement.statementProperties =
110+
StatementProperties.builder().querySettings(null).build();
111+
112+
ZoneId result = statement.resolveSessionTimeZone();
113+
assertThat(result).isEqualTo(ZoneId.systemDefault());
114+
}
105115
}

jdbc-core/src/test/java/com/salesforce/datacloud/jdbc/core/TimeZoneIntegrationTest.java

Lines changed: 254 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.sql.ResultSet;
1313
import java.sql.Statement;
1414
import java.sql.Timestamp;
15+
import java.sql.Types;
1516
import java.time.Instant;
1617
import java.time.LocalDateTime;
1718
import java.time.OffsetDateTime;
@@ -26,6 +27,8 @@
2627
import java.util.stream.Stream;
2728
import lombok.SneakyThrows;
2829
import lombok.val;
30+
import org.junit.jupiter.api.AfterAll;
31+
import org.junit.jupiter.api.BeforeAll;
2932
import org.junit.jupiter.api.Test;
3033
import org.junit.jupiter.api.extension.ExtendWith;
3134
import org.junit.jupiter.params.ParameterizedTest;
@@ -35,15 +38,61 @@
3538
/**
3639
* Integration tests for timezone and timestamp handling in DataCloud JDBC driver.
3740
*
38-
* Tests the timezone precedence order:
41+
* <p>Tests the timezone precedence order:
3942
* 1. Calendar parameter (per-call setting)
4043
* 2. Arrow metadata timezone (from Hyper - TIMESTAMPTZ columns)
4144
* 3. Session timezone (query setting time_zone)
4245
* 4. System default
46+
*
47+
* <p>Also contains the PreparedStatement parameter-setting matrix: every combination of setter
48+
* method, Java type, and SQL cast target ({@code ?::timestamp} / {@code ?::timestamptz}).
4349
*/
4450
@ExtendWith(LocalHyperTestBase.class)
4551
public class TimeZoneIntegrationTest {
4652

53+
// ── Parameter matrix constants ────────────────────────────────────────────────────────────
54+
// Input: UTC instant 2024-06-15T21:30:45.123456Z
55+
// In JVM TZ (LA, UTC-7 in June): wall-clock = "2024-06-15 14:30:45.123456"
56+
private static final Instant MATRIX_INPUT_INSTANT = Instant.parse("2024-06-15T21:30:45.123456Z");
57+
private static final LocalDateTime MATRIX_INPUT_LDT =
58+
LocalDateTime.ofInstant(MATRIX_INPUT_INSTANT, ZoneId.of("America/Los_Angeles")); // 14:30:45
59+
private static final OffsetDateTime MATRIX_INPUT_ODT =
60+
OffsetDateTime.ofInstant(MATRIX_INPUT_INSTANT, ZoneOffset.UTC);
61+
private static final ZonedDateTime MATRIX_INPUT_ZDT = ZonedDateTime.ofInstant(MATRIX_INPUT_INSTANT, ZoneOffset.UTC);
62+
private static final Timestamp MATRIX_INPUT_TS = Timestamp.from(MATRIX_INPUT_INSTANT);
63+
64+
/** 14:30:45 — JVM (LA) wall-clock digits, stored as a naive literal. */
65+
private static final LocalDateTime MATRIX_WALL_CLOCK_LA =
66+
LocalDateTime.ofInstant(MATRIX_INPUT_INSTANT, ZoneId.of("America/Los_Angeles"));
67+
68+
/** 21:30:45 — UTC wall-clock digits (= the instant read in UTC). */
69+
private static final LocalDateTime MATRIX_WALL_CLOCK_UTC =
70+
LocalDateTime.ofInstant(MATRIX_INPUT_INSTANT, ZoneOffset.UTC);
71+
72+
/** 21:30:45Z — the original UTC instant, preserved exactly. */
73+
private static final OffsetDateTime MATRIX_INSTANT_UTC =
74+
OffsetDateTime.ofInstant(MATRIX_INPUT_INSTANT, ZoneOffset.UTC);
75+
76+
/**
77+
* 14:30:45Z — wall-clock literal (14:30) encoded as naive UTC epoch, then cast to timestamptz
78+
* with session TZ=UTC. Diverges from PG which sends the true UTC epoch (21:30:45Z).
79+
*/
80+
private static final OffsetDateTime MATRIX_WALL_CLOCK_AS_UTC =
81+
OffsetDateTime.of(MATRIX_WALL_CLOCK_LA, ZoneOffset.UTC);
82+
83+
private static TimeZone matrixOriginalTimeZone;
84+
85+
@BeforeAll
86+
static void pinJvmTimezoneForMatrix() {
87+
matrixOriginalTimeZone = TimeZone.getDefault();
88+
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
89+
}
90+
91+
@AfterAll
92+
static void restoreJvmTimezoneAfterMatrix() {
93+
TimeZone.setDefault(matrixOriginalTimeZone);
94+
}
95+
4796
@Test
4897
@SneakyThrows
4998
public void testSessionTimezoneResolution() {
@@ -679,4 +728,208 @@ public void testTimestampRoundtrip(String castType, String sessionTz, String wri
679728
}
680729
}
681730
}
731+
732+
// ── PreparedStatement parameter-setting matrix ────────────────────────────────────────────
733+
734+
@FunctionalInterface
735+
interface ParameterSetter {
736+
void set(PreparedStatement pstmt) throws Exception;
737+
}
738+
739+
/**
740+
* Matrix of all supported setter/type/SQL-cast combinations.
741+
*
742+
* <p>Each row: (description, setter, castType, expectedLDT, expectedODT)
743+
* <ul>
744+
* <li>{@code expectedLDT} non-null → {@code ?::timestamp}: asserts
745+
* {@code getObject(LocalDateTime.class)}</li>
746+
* <li>{@code expectedODT} non-null → {@code ?::timestamptz}: asserts
747+
* {@code getObject(OffsetDateTime.class)}</li>
748+
* </ul>
749+
*
750+
* <p>JVM TZ is pinned to {@code America/Los_Angeles} (UTC-7 in June) so that
751+
* timezone-sensitive differences are visible. Session TZ is UTC for all queries.
752+
*
753+
* <h2>JDBC 4.2 Spec mapping (Table B-4)</h2>
754+
* <pre>
755+
* java.sql.Timestamp → TIMESTAMP (wall-clock in JVM TZ)
756+
* java.time.Instant → TIMESTAMP_WITH_TIMEZONE (UTC epoch)
757+
* java.time.LocalDateTime → TIMESTAMP (LDT digits stored as-is)
758+
* java.time.OffsetDateTime → TIMESTAMP_WITH_TIMEZONE (UTC epoch)
759+
* java.time.ZonedDateTime → TIMESTAMP_WITH_TIMEZONE (UTC epoch)
760+
* </pre>
761+
*/
762+
static Stream<Arguments> parameterMatrixCases() {
763+
return Stream.of(
764+
// ── setTimestamp (no calendar) ─────────────────────────────────────────────
765+
// Wall-clock normalization: JVM TZ (LA) wall-clock = 14:30:45 stored as naive literal.
766+
Arguments.of(
767+
"setTimestamp(ts) → ?::timestamp",
768+
(ParameterSetter) pstmt -> pstmt.setTimestamp(1, MATRIX_INPUT_TS),
769+
"timestamp",
770+
MATRIX_WALL_CLOCK_LA,
771+
null),
772+
// ?::timestamptz: Hyper interprets naive literal 14:30:45 in session UTC → 14:30:45Z.
773+
// (PG JDBC diverges: sends true UTC epoch 21:30:45Z instead.)
774+
Arguments.of(
775+
"setTimestamp(ts) → ?::timestamptz",
776+
(ParameterSetter) pstmt -> pstmt.setTimestamp(1, MATRIX_INPUT_TS),
777+
"timestamptz",
778+
null,
779+
MATRIX_WALL_CLOCK_AS_UTC),
780+
781+
// ── setTimestamp with Calendar UTC ─────────────────────────────────────────
782+
// Calendar UTC wall-clock = 21:30:45. Stored as naive literal 21:30:45.
783+
Arguments.of(
784+
"setTimestamp(ts, calUTC) → ?::timestamp",
785+
(ParameterSetter) pstmt -> pstmt.setTimestamp(
786+
1, MATRIX_INPUT_TS, Calendar.getInstance(TimeZone.getTimeZone("UTC"))),
787+
"timestamp",
788+
MATRIX_WALL_CLOCK_UTC,
789+
null),
790+
791+
// ── setObject(Timestamp) ───────────────────────────────────────────────────
792+
// Delegates to setTimestamp(ts). JVM wall-clock 14:30:45.
793+
Arguments.of(
794+
"setObject(Timestamp) → ?::timestamp",
795+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_TS),
796+
"timestamp",
797+
MATRIX_WALL_CLOCK_LA,
798+
null),
799+
800+
// ── setObject(Instant) ────────────────────────────────────────────────────
801+
// JDBC 4.2: Instant → TIMESTAMP_WITH_TIMEZONE. UTC epoch stored exactly.
802+
Arguments.of(
803+
"setObject(Instant) → ?::timestamptz",
804+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_INSTANT),
805+
"timestamptz",
806+
null,
807+
MATRIX_INSTANT_UTC),
808+
Arguments.of(
809+
"setObject(Instant) → ?::timestamp",
810+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_INSTANT),
811+
"timestamp",
812+
MATRIX_WALL_CLOCK_UTC,
813+
null),
814+
815+
// ── setObject(LocalDateTime) ──────────────────────────────────────────────
816+
// JDBC 4.2: LocalDateTime → TIMESTAMP. LDT digits stored as-is (no TZ shift).
817+
// Recommended write path for wall-clock (TIMESTAMP without timezone) values.
818+
Arguments.of(
819+
"setObject(LocalDateTime) → ?::timestamp",
820+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_LDT),
821+
"timestamp",
822+
MATRIX_WALL_CLOCK_LA,
823+
null),
824+
825+
// ── setObject(OffsetDateTime) ─────────────────────────────────────────────
826+
// JDBC 4.2: OffsetDateTime → TIMESTAMP_WITH_TIMEZONE. UTC epoch stored.
827+
// Recommended write path for exact-instant (TIMESTAMPTZ) values.
828+
Arguments.of(
829+
"setObject(OffsetDateTime) → ?::timestamptz",
830+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_ODT),
831+
"timestamptz",
832+
null,
833+
MATRIX_INSTANT_UTC),
834+
Arguments.of(
835+
"setObject(OffsetDateTime) → ?::timestamp",
836+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_ODT),
837+
"timestamp",
838+
MATRIX_WALL_CLOCK_UTC,
839+
null),
840+
841+
// ── setObject(ZonedDateTime) ──────────────────────────────────────────────
842+
// JDBC 4.2: ZonedDateTime → TIMESTAMP_WITH_TIMEZONE. UTC epoch stored.
843+
Arguments.of(
844+
"setObject(ZonedDateTime) → ?::timestamptz",
845+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_ZDT),
846+
"timestamptz",
847+
null,
848+
MATRIX_INSTANT_UTC),
849+
850+
// ── setObject(ts, Types.TIMESTAMP) ────────────────────────────────────────
851+
// Explicit TIMESTAMP type hint. JVM wall-clock 14:30:45.
852+
Arguments.of(
853+
"setObject(ts, TIMESTAMP) → ?::timestamp",
854+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_TS, Types.TIMESTAMP),
855+
"timestamp",
856+
MATRIX_WALL_CLOCK_LA,
857+
null),
858+
859+
// ── setObject(ts, Types.TIMESTAMP_WITH_TIMEZONE) ──────────────────────────
860+
// Explicit TIMESTAMPTZ type hint. UTC epoch from Timestamp.toInstant() stored.
861+
// Use this when you have a java.sql.Timestamp but need TIMESTAMPTZ roundtrip.
862+
Arguments.of(
863+
"setObject(ts, TIMESTAMP_WITH_TIMEZONE) → ?::timestamptz",
864+
(ParameterSetter) pstmt -> pstmt.setObject(1, MATRIX_INPUT_TS, Types.TIMESTAMP_WITH_TIMEZONE),
865+
"timestamptz",
866+
null,
867+
MATRIX_INSTANT_UTC));
868+
}
869+
870+
@ParameterizedTest(name = "{0}")
871+
@MethodSource("parameterMatrixCases")
872+
@SneakyThrows
873+
void verifyParameterMatrix(
874+
String description,
875+
ParameterSetter setter,
876+
String castType,
877+
LocalDateTime expectedLDT,
878+
OffsetDateTime expectedODT) {
879+
880+
Properties props = new Properties();
881+
props.setProperty("querySetting.time_zone", "UTC");
882+
883+
try (DataCloudConnection conn = LocalHyperTestBase.getHyperQueryConnection(props)) {
884+
String sql = "SELECT (?::" + castType + ") AS val";
885+
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
886+
setter.set(pstmt);
887+
try (ResultSet rs = pstmt.executeQuery()) {
888+
assertThat(rs.next()).isTrue();
889+
890+
if (expectedLDT != null) {
891+
assertThat(rs.getObject("val", LocalDateTime.class))
892+
.as("%s — getObject(LocalDateTime.class)", description)
893+
.isEqualTo(expectedLDT);
894+
}
895+
896+
if (expectedODT != null) {
897+
assertThat(rs.getObject("val", OffsetDateTime.class))
898+
.as("%s — getObject(OffsetDateTime.class)", description)
899+
.isEqualTo(expectedODT);
900+
}
901+
}
902+
}
903+
}
904+
}
905+
906+
@Test
907+
@SneakyThrows
908+
void nullTimestampParameterStoresSqlNull() {
909+
try (DataCloudConnection conn = LocalHyperTestBase.getHyperQueryConnection(new Properties())) {
910+
try (PreparedStatement pstmt = conn.prepareStatement("SELECT (?::timestamp) AS val")) {
911+
pstmt.setNull(1, Types.TIMESTAMP);
912+
try (ResultSet rs = pstmt.executeQuery()) {
913+
assertThat(rs.next()).isTrue();
914+
assertThat(rs.getObject("val")).isNull();
915+
assertThat(rs.wasNull()).isTrue();
916+
}
917+
}
918+
}
919+
}
920+
921+
@Test
922+
@SneakyThrows
923+
void nullTimestampTZParameterStoresSqlNull() {
924+
try (DataCloudConnection conn = LocalHyperTestBase.getHyperQueryConnection(new Properties())) {
925+
try (PreparedStatement pstmt = conn.prepareStatement("SELECT (?::timestamptz) AS val")) {
926+
pstmt.setNull(1, Types.TIMESTAMP_WITH_TIMEZONE);
927+
try (ResultSet rs = pstmt.executeQuery()) {
928+
assertThat(rs.next()).isTrue();
929+
assertThat(rs.getObject("val")).isNull();
930+
assertThat(rs.wasNull()).isTrue();
931+
}
932+
}
933+
}
934+
}
682935
}

0 commit comments

Comments
 (0)