Skip to content

Commit 542ac4b

Browse files
SNOW-1950032 Fix Julian/Gregorian timestamps in bind uploader
1 parent 9a99cdf commit 542ac4b

File tree

2 files changed

+133
-38
lines changed

2 files changed

+133
-38
lines changed

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

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,17 @@
88
import java.io.InputStream;
99
import java.nio.ByteBuffer;
1010
import java.sql.SQLException;
11-
import java.sql.Time;
12-
import java.sql.Timestamp;
13-
import java.text.DateFormat;
14-
import java.text.SimpleDateFormat;
1511
import java.time.Instant;
1612
import java.time.LocalDate;
13+
import java.time.LocalTime;
1714
import java.time.ZoneId;
1815
import java.time.ZoneOffset;
16+
import java.time.ZonedDateTime;
1917
import java.time.format.DateTimeFormatter;
18+
import java.time.format.DateTimeFormatterBuilder;
2019
import java.util.ArrayList;
21-
import java.util.Calendar;
22-
import java.util.GregorianCalendar;
2320
import java.util.List;
2421
import java.util.Map;
25-
import java.util.TimeZone;
2622
import net.snowflake.client.core.ExecTimeTelemetryData;
2723
import net.snowflake.client.core.ParameterBindingDTO;
2824
import net.snowflake.client.core.SFBaseSession;
@@ -54,10 +50,14 @@ public class BindUploader implements Closeable {
5450

5551
private int fileCount = 0;
5652

57-
private final DateFormat utcTimestampFormat;
58-
private final DateFormat localTimestampFormat;
59-
private final DateTimeFormatter dateFormat;
60-
private final SimpleDateFormat timeFormat;
53+
private final DateTimeFormatter timestampFormatter =
54+
new DateTimeFormatterBuilder()
55+
.append(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS "))
56+
.appendOffset("+HH:MM", "Z")
57+
.toFormatter();
58+
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
59+
private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS");
60+
6161
private final String createStageSQL;
6262

6363
static class ColumnTypeDataPair {
@@ -87,20 +87,6 @@ private BindUploader(SFBaseSession session, String stageDir) {
8787
+ " type=csv"
8888
+ " field_optionally_enclosed_by='\"'"
8989
+ ")";
90-
91-
Calendar utcCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
92-
utcCalendar.clear();
93-
94-
Calendar localCalendar = new GregorianCalendar(TimeZone.getDefault());
95-
localCalendar.clear();
96-
97-
this.utcTimestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.");
98-
this.utcTimestampFormat.setCalendar(utcCalendar);
99-
this.localTimestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.");
100-
this.localTimestampFormat.setCalendar(localCalendar);
101-
this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
102-
this.timeFormat = new SimpleDateFormat("HH:mm:ss.");
103-
this.timeFormat.setCalendar(utcCalendar);
10490
}
10591

10692
private synchronized String synchronizedDateFormat(String o) {
@@ -110,7 +96,7 @@ private synchronized String synchronizedDateFormat(String o) {
11096
long millis = Long.parseLong(o);
11197
Instant instant = Instant.ofEpochMilli(millis);
11298
LocalDate localDate = instant.atZone(ZoneOffset.UTC).toLocalDate();
113-
return localDate.format(this.dateFormat);
99+
return localDate.format(this.dateFormatter);
114100
}
115101

116102
private synchronized String synchronizedTimeFormat(String o) {
@@ -121,10 +107,8 @@ private synchronized String synchronizedTimeFormat(String o) {
121107
long sec = times.left;
122108
int nano = times.right;
123109

124-
Time v1 = new Time(sec * 1000);
125-
String formatWithDate = utcTimestampFormat.format(v1) + String.format("%09d", nano);
126-
// Take out the Date portion of the formatted string. Only time data is needed.
127-
return formatWithDate.substring(11);
110+
LocalTime time = Instant.ofEpochSecond(sec, nano).atZone(ZoneOffset.UTC).toLocalTime();
111+
return time.format(timeFormatter);
128112
}
129113

130114
private SFPair<Long, Integer> getNanosAndSecs(String o, boolean isNegative) {
@@ -163,16 +147,16 @@ private synchronized String synchronizedTimestampFormat(String o, String type) {
163147
long sec = times.left;
164148
int nano = times.right;
165149

166-
Timestamp v1 = new Timestamp(sec * 1000);
167-
ZoneOffset offsetId;
150+
Instant instant = Instant.ofEpochSecond(sec, nano);
151+
168152
// For timestamp_ntz, use UTC timezone. For timestamp_ltz, use the local timezone to minimise
169153
// the gap.
170154
if ("TIMESTAMP_LTZ".equals(type)) {
171-
offsetId = ZoneId.systemDefault().getRules().getOffset(Instant.ofEpochMilli(v1.getTime()));
172-
return localTimestampFormat.format(v1) + String.format("%09d", nano) + " " + offsetId;
155+
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
156+
return zdt.format(timestampFormatter);
173157
} else {
174-
offsetId = ZoneOffset.UTC;
175-
return utcTimestampFormat.format(v1) + String.format("%09d", nano) + " " + offsetId;
158+
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC);
159+
return zdt.format(timestampFormatter);
176160
}
177161
}
178162

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

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import net.snowflake.client.util.SFPair;
2727
import org.junit.jupiter.api.Tag;
2828
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.ValueSource;
2931

3032
/**
3133
* Binding Data integration tests for the latest JDBC driver. This doesn't work for the oldest
@@ -374,7 +376,7 @@ public void testTimestampLtzBindingNoLongerBreaksOtherDatetimeBindings() throws
374376
}
375377

376378
@Test
377-
public void testGregorianJulianConversions() throws SQLException {
379+
public void testGregorianJulianDateConversions() throws SQLException {
378380
try (Connection connection = getConnection();
379381
Statement statement = connection.createStatement()) {
380382
statement.execute("create or replace table stageinsertdates(ind int, d1 date)");
@@ -401,6 +403,56 @@ public void testGregorianJulianConversions() throws SQLException {
401403
}
402404
}
403405

406+
@ParameterizedTest
407+
@ValueSource(strings = {"TIMESTAMP_LTZ", "TIMESTAMP_NTZ"})
408+
public void testGregorianJulianTimestampConversions(String timestampType) throws SQLException {
409+
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
410+
List<Timestamp> gregorianJulianTimestamps =
411+
Arrays.asList(
412+
Timestamp.valueOf("0001-01-01 00:00:00"),
413+
Timestamp.valueOf("0100-03-01 00:00:00"),
414+
Timestamp.valueOf("0400-02-29 00:00:00"),
415+
Timestamp.valueOf("0400-03-01 00:00:00"),
416+
Timestamp.valueOf("1582-10-15 00:00:00"),
417+
Timestamp.valueOf("1900-02-28 00:00:00"),
418+
Timestamp.valueOf("1900-03-01 00:00:00"),
419+
Timestamp.valueOf("1969-12-31 00:00:00"),
420+
Timestamp.valueOf("1970-01-01 00:00:00"),
421+
Timestamp.valueOf("2000-02-28 00:00:00"),
422+
Timestamp.valueOf("2000-02-29 00:00:00"),
423+
Timestamp.valueOf("2000-03-01 00:00:00"),
424+
Timestamp.valueOf("2023-10-26 00:00:00"),
425+
Timestamp.valueOf("2024-02-29 00:00:00"));
426+
try (Connection connection = getConnection();
427+
Statement statement = connection.createStatement()) {
428+
statement.execute(
429+
"create or replace table stageinsertdates(ind int, d1 " + timestampType + ")");
430+
statement.execute("alter session set CLIENT_TIMESTAMP_TYPE_MAPPING=" + timestampType);
431+
432+
try (PreparedStatement prepStatement =
433+
connection.prepareStatement("insert into stageinsertdates values (?,?)")) {
434+
for (int i = 0; i < gregorianJulianTimestamps.size(); i++) {
435+
prepStatement.setInt(1, i);
436+
prepStatement.setTimestamp(2, gregorianJulianTimestamps.get(i));
437+
prepStatement.addBatch();
438+
}
439+
440+
prepStatement.executeBatch();
441+
prepStatement.getConnection().commit();
442+
}
443+
444+
try (ResultSet rs1 = statement.executeQuery("select * from stageinsertdates order by ind")) {
445+
for (int i = 0; i < gregorianJulianTimestamps.size(); i++) {
446+
assertTrue(rs1.next());
447+
assertEquals(i, rs1.getInt(1));
448+
assertEquals(gregorianJulianTimestamps.get(i), rs1.getTimestamp(2));
449+
}
450+
}
451+
} finally {
452+
TimeZone.setDefault(origTz);
453+
}
454+
}
455+
404456
/**
405457
* This test cannot run on the GitHub testing because of the "ALTER SESSION SET
406458
* CLIENT_STAGE_ARRAY_BINDING_THRESHOLD" This command should be executed with the system admin.
@@ -409,7 +461,7 @@ public void testGregorianJulianConversions() throws SQLException {
409461
*/
410462
@Test
411463
@DontRunOnGithubActions
412-
public void testGregorianJulianConversionsWithStageBindings() throws SQLException {
464+
public void testGregorianJulianDateConversionsWithStageBindings() throws SQLException {
413465
try (Connection connection = getConnection();
414466
Statement statement = connection.createStatement()) {
415467
statement.execute("create or replace table stageinsertdates(ind int, d1 date)");
@@ -437,6 +489,65 @@ public void testGregorianJulianConversionsWithStageBindings() throws SQLExceptio
437489
}
438490
}
439491

492+
/**
493+
* This test cannot run on the GitHub testing because of the "ALTER SESSION SET
494+
* CLIENT_STAGE_ARRAY_BINDING_THRESHOLD" This command should be executed with the system admin.
495+
*
496+
* @throws SQLException
497+
*/
498+
@ParameterizedTest
499+
@ValueSource(strings = {"TIMESTAMP_LTZ", "TIMESTAMP_NTZ"})
500+
@DontRunOnGithubActions
501+
public void testGregorianJulianTimestampConversionsWithStageBindings(String timestampType)
502+
throws SQLException {
503+
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
504+
List<Timestamp> gregorianJulianTimestamps =
505+
Arrays.asList(
506+
Timestamp.valueOf("0001-01-01 00:00:00"),
507+
Timestamp.valueOf("0100-03-01 00:00:00"),
508+
Timestamp.valueOf("0400-02-29 00:00:00"),
509+
Timestamp.valueOf("0400-03-01 00:00:00"),
510+
Timestamp.valueOf("1582-10-15 00:00:00"),
511+
Timestamp.valueOf("1900-02-28 00:00:00"),
512+
Timestamp.valueOf("1900-03-01 00:00:00"),
513+
Timestamp.valueOf("1969-12-31 00:00:00"),
514+
Timestamp.valueOf("1970-01-01 00:00:00"),
515+
Timestamp.valueOf("2000-02-28 00:00:00"),
516+
Timestamp.valueOf("2000-02-29 00:00:00"),
517+
Timestamp.valueOf("2000-03-01 00:00:00"),
518+
Timestamp.valueOf("2023-10-26 00:00:00"),
519+
Timestamp.valueOf("2024-02-29 00:00:00"));
520+
try (Connection connection = getConnection();
521+
Statement statement = connection.createStatement()) {
522+
statement.execute(
523+
"create or replace table stageinsertdates(ind int, d1 " + timestampType + ")");
524+
statement.execute("alter session set CLIENT_STAGE_ARRAY_BINDING_THRESHOLD = 1");
525+
statement.execute("alter session set CLIENT_TIMESTAMP_TYPE_MAPPING=" + timestampType);
526+
527+
try (PreparedStatement prepStatement =
528+
connection.prepareStatement("insert into stageinsertdates values (?,?)")) {
529+
for (int i = 0; i < gregorianJulianTimestamps.size(); i++) {
530+
prepStatement.setInt(1, i);
531+
prepStatement.setTimestamp(2, gregorianJulianTimestamps.get(i));
532+
prepStatement.addBatch();
533+
}
534+
535+
prepStatement.executeBatch();
536+
prepStatement.getConnection().commit();
537+
}
538+
539+
try (ResultSet rs1 = statement.executeQuery("select * from stageinsertdates order by ind")) {
540+
for (int i = 0; i < gregorianJulianTimestamps.size(); i++) {
541+
assertTrue(rs1.next());
542+
assertEquals(i, rs1.getInt(1));
543+
assertEquals(gregorianJulianTimestamps.get(i), rs1.getTimestamp(2));
544+
}
545+
}
546+
} finally {
547+
TimeZone.setDefault(origTz);
548+
}
549+
}
550+
440551
@Test
441552
public void testInsertTimeColumnAsWallClockTimeRegardlessOfTimezone() throws SQLException {
442553
TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Honolulu"));

0 commit comments

Comments
 (0)