Skip to content

Commit c9af575

Browse files
SNOW-1950032 Fix Gregorian dates handling in BindUploader and handling of TIME as wall clock time (#2164)
1 parent 94ac08c commit c9af575

File tree

7 files changed

+266
-9
lines changed

7 files changed

+266
-9
lines changed

src/main/java/net/snowflake/client/core/JsonSqlOutput.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,13 @@ public void writeDate(Date value) throws SQLException {
198198
public void writeTime(Time x) throws SQLException {
199199
withNextValue(
200200
((json, fieldName, maybeColumn) -> {
201-
long nanosSinceMidnight = SfTimestampUtil.getTimeInNanoseconds(x);
201+
long nanosSinceMidnight;
202+
if (session.getTreatTimeAsWallClockTime()) {
203+
nanosSinceMidnight = x.toLocalTime().toNanoOfDay();
204+
} else {
205+
nanosSinceMidnight = SfTimestampUtil.getTimeInNanoseconds(x);
206+
}
207+
202208
String result =
203209
ResultUtil.getSFTimeAsString(
204210
SFTime.fromNanoseconds(nanosSinceMidnight),

src/main/java/net/snowflake/client/core/SFBaseSession.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ public abstract class SFBaseSession {
151151

152152
private boolean clearBatchOnlyAfterSuccessfulExecution = false;
153153

154+
/** Treat java.sql.Time as wall clock time without converting it to UTC */
155+
private boolean treatTimeAsWallClockTime = false;
156+
154157
protected SFBaseSession(SFConnectionHandler sfConnectionHandler) {
155158
this.sfConnectionHandler = sfConnectionHandler;
156159
}
@@ -1362,4 +1365,12 @@ void setClearBatchOnlyAfterSuccessfulExecution(boolean value) {
13621365
public boolean getClearBatchOnlyAfterSuccessfulExecution() {
13631366
return this.clearBatchOnlyAfterSuccessfulExecution;
13641367
}
1368+
1369+
public boolean getTreatTimeAsWallClockTime() {
1370+
return treatTimeAsWallClockTime;
1371+
}
1372+
1373+
public void setTreatTimeAsWallClockTime(boolean treatTimeAsWallClockTime) {
1374+
this.treatTimeAsWallClockTime = treatTimeAsWallClockTime;
1375+
}
13651376
}

src/main/java/net/snowflake/client/core/SFSession.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,12 @@ public void addSFSessionProperty(String propertyName, Object propertyValue) thro
569569
}
570570
break;
571571

572+
case CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME:
573+
if (propertyValue != null) {
574+
setTreatTimeAsWallClockTime(getBooleanValue(propertyValue));
575+
}
576+
break;
577+
572578
default:
573579
break;
574580
}

src/main/java/net/snowflake/client/core/SFSessionProperty.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ public enum SFSessionProperty {
129129
IMPLICIT_SERVER_SIDE_QUERY_TIMEOUT("IMPLICIT_SERVER_SIDE_QUERY_TIMEOUT", false, Boolean.class),
130130

131131
CLEAR_BATCH_ONLY_AFTER_SUCCESSFUL_EXECUTION(
132-
"CLEAR_BATCH_ONLY_AFTER_SUCCESSFUL_EXECUTION", false, Boolean.class);
132+
"CLEAR_BATCH_ONLY_AFTER_SUCCESSFUL_EXECUTION", false, Boolean.class),
133+
134+
CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME(
135+
"CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME", false, Boolean.class);
133136

134137
// property key in string
135138
private String propertyKey;

src/main/java/net/snowflake/client/core/bind/BindUploader.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import java.text.DateFormat;
1414
import java.text.SimpleDateFormat;
1515
import java.time.Instant;
16+
import java.time.LocalDate;
1617
import java.time.ZoneId;
1718
import java.time.ZoneOffset;
19+
import java.time.format.DateTimeFormatter;
1820
import java.util.ArrayList;
1921
import java.util.Calendar;
2022
import java.util.GregorianCalendar;
@@ -54,7 +56,7 @@ public class BindUploader implements Closeable {
5456

5557
private final DateFormat utcTimestampFormat;
5658
private final DateFormat localTimestampFormat;
57-
private final DateFormat dateFormat;
59+
private final DateTimeFormatter dateFormat;
5860
private final SimpleDateFormat timeFormat;
5961
private final String createStageSQL;
6062

@@ -96,8 +98,7 @@ private BindUploader(SFBaseSession session, String stageDir) {
9698
this.utcTimestampFormat.setCalendar(utcCalendar);
9799
this.localTimestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.");
98100
this.localTimestampFormat.setCalendar(localCalendar);
99-
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
100-
this.dateFormat.setCalendar(utcCalendar);
101+
this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
101102
this.timeFormat = new SimpleDateFormat("HH:mm:ss.");
102103
this.timeFormat.setCalendar(utcCalendar);
103104
}
@@ -106,7 +107,10 @@ private synchronized String synchronizedDateFormat(String o) {
106107
if (o == null) {
107108
return null;
108109
}
109-
return dateFormat.format(new java.sql.Date(Long.parseLong(o)));
110+
long millis = Long.parseLong(o);
111+
Instant instant = Instant.ofEpochMilli(millis);
112+
LocalDate localDate = instant.atZone(ZoneOffset.UTC).toLocalDate();
113+
return localDate.format(this.dateFormat);
110114
}
111115

112116
private synchronized String synchronizedTimeFormat(String o) {

src/main/java/net/snowflake/client/jdbc/SnowflakePreparedStatementV1.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,17 @@ public void setTime(int parameterIndex, Time x) throws SQLException {
382382
if (x == null) {
383383
setNull(parameterIndex, Types.TIME);
384384
} else {
385-
// Convert to nanoseconds since midnight using the input time mod 24 hours.
386-
long nanosSinceMidnight = SfTimestampUtil.getTimeInNanoseconds(x);
385+
String value;
386+
if (connection.getSFBaseSession().getTreatTimeAsWallClockTime()) {
387+
value = String.valueOf(x.toLocalTime().toNanoOfDay());
388+
} else {
389+
value = String.valueOf(SfTimestampUtil.getTimeInNanoseconds(x));
390+
}
387391

388392
ParameterBindingDTO binding =
389393
new ParameterBindingDTO(
390394
SnowflakeUtil.javaTypeToSFTypeString(Types.TIME, connection.getSFBaseSession()),
391-
String.valueOf(nanosSinceMidnight));
395+
value);
392396

393397
parameterBindings.put(String.valueOf(parameterIndex), binding);
394398
}

src/test/java/net/snowflake/client/jdbc/BindingDataLatestIT.java

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@
1111
import java.sql.ResultSet;
1212
import java.sql.SQLException;
1313
import java.sql.Statement;
14+
import java.sql.Time;
1415
import java.sql.Timestamp;
1516
import java.time.ZoneId;
1617
import java.time.ZonedDateTime;
18+
import java.util.Arrays;
1719
import java.util.Calendar;
20+
import java.util.List;
21+
import java.util.Properties;
1822
import java.util.TimeZone;
1923
import net.snowflake.client.AbstractDriverIT;
2024
import net.snowflake.client.annotations.DontRunOnGithubActions;
2125
import net.snowflake.client.category.TestTags;
26+
import net.snowflake.client.util.SFPair;
2227
import org.junit.jupiter.api.Tag;
2328
import org.junit.jupiter.api.Test;
2429

@@ -35,6 +40,34 @@ public class BindingDataLatestIT extends AbstractDriverIT {
3540
TimeZone australiaTz = TimeZone.getTimeZone("Australia/Sydney");
3641
Calendar tokyo = Calendar.getInstance(tokyoTz);
3742

43+
List<Time> times =
44+
Arrays.asList(
45+
Time.valueOf("00:00:00"),
46+
Time.valueOf("11:59:59"),
47+
Time.valueOf("12:00:00"),
48+
Time.valueOf("12:34:56"),
49+
Time.valueOf("13:01:01"),
50+
Time.valueOf("15:30:00"),
51+
Time.valueOf("23:59:59"));
52+
53+
List<SFPair<String, Date>> gregorianJulianDates =
54+
Arrays.asList(
55+
SFPair.of("0001-01-01", Date.valueOf("0001-01-01")),
56+
SFPair.of("0100-03-01", Date.valueOf("0100-03-01")),
57+
SFPair.of("0400-02-29", Date.valueOf("0400-02-29")),
58+
SFPair.of("0400-03-01", Date.valueOf("0400-03-01")),
59+
SFPair.of("1400-03-01", Date.valueOf("1400-03-01")),
60+
SFPair.of("1582-10-15", Date.valueOf("1582-10-15")),
61+
SFPair.of("1900-02-28", Date.valueOf("1900-02-28")),
62+
SFPair.of("1900-03-01", Date.valueOf("1900-03-01")),
63+
SFPair.of("1969-12-31", Date.valueOf("1969-12-31")),
64+
SFPair.of("1970-01-01", Date.valueOf("1970-01-01")),
65+
SFPair.of("2000-02-28", Date.valueOf("2000-02-28")),
66+
SFPair.of("2000-02-29", Date.valueOf("2000-02-29")),
67+
SFPair.of("2000-03-01", Date.valueOf("2000-03-01")),
68+
SFPair.of("2023-10-26", Date.valueOf("2023-10-26")),
69+
SFPair.of("2024-02-29", Date.valueOf("2024-02-29")));
70+
3871
@Test
3972
public void testBindTimestampTZ() throws SQLException {
4073
try (Connection connection = getConnection();
@@ -340,6 +373,196 @@ public void testTimestampLtzBindingNoLongerBreaksOtherDatetimeBindings() throws
340373
}
341374
}
342375

376+
@Test
377+
public void testGregorianJulianConversions() throws SQLException {
378+
try (Connection connection = getConnection();
379+
Statement statement = connection.createStatement()) {
380+
statement.execute("create or replace table stageinsertdates(ind int, d1 date)");
381+
382+
try (PreparedStatement prepStatement =
383+
connection.prepareStatement("insert into stageinsertdates values (?,?)")) {
384+
for (int i = 0; i < gregorianJulianDates.size(); i++) {
385+
prepStatement.setInt(1, i);
386+
prepStatement.setDate(2, gregorianJulianDates.get(i).right);
387+
prepStatement.addBatch();
388+
}
389+
390+
prepStatement.executeBatch();
391+
prepStatement.getConnection().commit();
392+
}
393+
394+
try (ResultSet rs1 = statement.executeQuery("select * from stageinsertdates order by ind")) {
395+
for (int i = 0; i < gregorianJulianDates.size(); i++) {
396+
assertTrue(rs1.next());
397+
assertEquals(i, rs1.getInt(1));
398+
assertEquals(gregorianJulianDates.get(i).left, rs1.getDate(2).toLocalDate().toString());
399+
}
400+
}
401+
}
402+
}
403+
404+
/**
405+
* This test cannot run on the GitHub testing because of the "ALTER SESSION SET
406+
* CLIENT_STAGE_ARRAY_BINDING_THRESHOLD" This command should be executed with the system admin.
407+
*
408+
* @throws SQLException
409+
*/
410+
@Test
411+
@DontRunOnGithubActions
412+
public void testGregorianJulianConversionsWithStageBindings() throws SQLException {
413+
try (Connection connection = getConnection();
414+
Statement statement = connection.createStatement()) {
415+
statement.execute("create or replace table stageinsertdates(ind int, d1 date)");
416+
statement.execute("alter session set CLIENT_STAGE_ARRAY_BINDING_THRESHOLD = 1");
417+
418+
try (PreparedStatement prepStatement =
419+
connection.prepareStatement("insert into stageinsertdates values (?,?)")) {
420+
for (int i = 0; i < gregorianJulianDates.size(); i++) {
421+
prepStatement.setInt(1, i);
422+
prepStatement.setDate(2, gregorianJulianDates.get(i).right);
423+
prepStatement.addBatch();
424+
}
425+
426+
prepStatement.executeBatch();
427+
prepStatement.getConnection().commit();
428+
}
429+
430+
try (ResultSet rs1 = statement.executeQuery("select * from stageinsertdates order by ind")) {
431+
for (int i = 0; i < gregorianJulianDates.size(); i++) {
432+
assertTrue(rs1.next());
433+
assertEquals(i, rs1.getInt(1));
434+
assertEquals(gregorianJulianDates.get(i).left, rs1.getDate(2).toLocalDate().toString());
435+
}
436+
}
437+
}
438+
}
439+
440+
@Test
441+
public void testInsertTimeColumnAsWallClockTimeRegardlessOfTimezone() throws SQLException {
442+
TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Honolulu"));
443+
444+
Properties props = new Properties();
445+
props.put("CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME", true);
446+
try (Connection connection = getConnection(DONT_INJECT_SOCKET_TIMEOUT, props, false, false);
447+
Statement statement = connection.createStatement()) {
448+
statement.execute("create or replace table test_wall_clock_time(ind int, t1 time)");
449+
statement.execute("alter session set TIMEZONE='America/Los_Angeles';");
450+
451+
try (PreparedStatement prepStatement =
452+
connection.prepareStatement("insert into test_wall_clock_time values (?,?)")) {
453+
for (int i = 0; i < times.size(); i++) {
454+
prepStatement.setInt(1, i);
455+
prepStatement.setTime(2, times.get(i));
456+
prepStatement.addBatch();
457+
}
458+
459+
prepStatement.executeBatch();
460+
prepStatement.getConnection().commit();
461+
}
462+
463+
try (ResultSet rs =
464+
statement.executeQuery("select * from test_wall_clock_time order by ind")) {
465+
for (int i = 0; i < times.size(); i++) {
466+
assertTrue(rs.next());
467+
assertEquals(i, rs.getInt(1));
468+
assertEquals(times.get(i).toLocalTime(), rs.getTime(2).toLocalTime());
469+
// check if inserted time is wall clock time
470+
assertEquals(times.get(i).toString(), rs.getString(2));
471+
}
472+
}
473+
} finally {
474+
TimeZone.setDefault(origTz);
475+
}
476+
}
477+
478+
/**
479+
* This test cannot run on the GitHub testing because of the "ALTER SESSION SET
480+
* CLIENT_STAGE_ARRAY_BINDING_THRESHOLD" This command should be executed with the system admin.
481+
*
482+
* @throws SQLException
483+
*/
484+
@Test
485+
@DontRunOnGithubActions
486+
public void testInsertTimeColumnAsWallClockTimeRegardlessOfTimezoneWithStageBinding()
487+
throws SQLException {
488+
TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Honolulu"));
489+
490+
Properties props = new Properties();
491+
props.put("CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME", true);
492+
try (Connection connection = getConnection(DONT_INJECT_SOCKET_TIMEOUT, props, false, false);
493+
Statement statement = connection.createStatement()) {
494+
statement.execute("create or replace table test_wall_clock_time(ind int, t1 time)");
495+
statement.execute("alter session set TIMEZONE='America/Los_Angeles';");
496+
statement.execute("alter session set CLIENT_STAGE_ARRAY_BINDING_THRESHOLD = 1");
497+
498+
try (PreparedStatement prepStatement =
499+
connection.prepareStatement("insert into test_wall_clock_time values (?,?)")) {
500+
for (int i = 0; i < times.size(); i++) {
501+
prepStatement.setInt(1, i);
502+
prepStatement.setTime(2, times.get(i));
503+
prepStatement.addBatch();
504+
}
505+
506+
prepStatement.executeBatch();
507+
prepStatement.getConnection().commit();
508+
}
509+
510+
try (ResultSet rs =
511+
statement.executeQuery("select * from test_wall_clock_time order by ind")) {
512+
for (int i = 0; i < times.size(); i++) {
513+
assertTrue(rs.next());
514+
assertEquals(i, rs.getInt(1));
515+
assertEquals(times.get(i).toLocalTime(), rs.getTime(2).toLocalTime());
516+
// check if inserted time is wall clock time
517+
assertEquals(times.get(i).toString(), rs.getString(2));
518+
}
519+
}
520+
} finally {
521+
TimeZone.setDefault(origTz);
522+
}
523+
}
524+
525+
/***
526+
* Verifies that without enabling CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME time column gets shifted to UTC on insert
527+
*
528+
* @throws SQLException
529+
*/
530+
@Test
531+
public void testInsertTimeColumnNotAsWallClockTimeAsUtc() throws SQLException {
532+
TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Honolulu"));
533+
534+
Properties props = new Properties();
535+
props.put("CLIENT_TREAT_TIME_AS_WALL_CLOCK_TIME", false);
536+
try (Connection connection = getConnection(DONT_INJECT_SOCKET_TIMEOUT, props, false, false);
537+
Statement statement = connection.createStatement()) {
538+
statement.execute("create or replace table test_wall_clock_time(ind int, t1 time)");
539+
statement.execute("alter session set TIMEZONE='America/Los_Angeles';");
540+
541+
String localTimeValue = "00:00:00";
542+
String utcTimeValue = "10:00:00"; // 00:00 in Pacific/Honolulu is 10:00 UTC
543+
Time localTime = Time.valueOf(localTimeValue);
544+
545+
try (PreparedStatement prepStatement =
546+
connection.prepareStatement("insert into test_wall_clock_time values (?,?)")) {
547+
prepStatement.setInt(1, 0);
548+
prepStatement.setTime(2, localTime);
549+
prepStatement.addBatch();
550+
prepStatement.executeBatch();
551+
prepStatement.getConnection().commit();
552+
}
553+
554+
try (ResultSet rs =
555+
statement.executeQuery("select * from test_wall_clock_time order by ind")) {
556+
assertTrue(rs.next());
557+
assertEquals(0, rs.getInt(1));
558+
// check if time gets shifted to UTC time on insert
559+
assertEquals(utcTimeValue, rs.getString(2));
560+
}
561+
} finally {
562+
TimeZone.setDefault(origTz);
563+
}
564+
}
565+
343566
public void executePsStatementForTimestampTest(
344567
Connection connection, String tableName, Timestamp timestamp) throws SQLException {
345568
try (PreparedStatement prepStatement =

0 commit comments

Comments
 (0)