Skip to content

Commit c513efa

Browse files
authored
Extended bulk copy API support for temporal and money datatypes (#2670)
* Extended bulk copy API support for temporal and money datatypes * Fixed test failure * Addressed comments * Updated test cases
1 parent 9a5477c commit c513efa

File tree

3 files changed

+234
-6
lines changed

3 files changed

+234
-6
lines changed

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1392,8 +1392,12 @@ private String getDestTypeFromSrcType(int srcColIndx, int destColIndx,
13921392
switch (destSSType) {
13931393
case SMALLDATETIME:
13941394
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
1395+
/*
1396+
* Fallback to maximum precision when sending smalldatetime as varchar.
1397+
* The default precision (16) is too small for the full string value and will cause issue.
1398+
*/
13951399
return SSType.VARCHAR.toString() + "("
1396-
+ ((0 == bulkPrecision) ? SOURCE_BULK_RECORD_TEMPORAL_MAX_PRECISION : bulkPrecision)
1400+
+ SOURCE_BULK_RECORD_TEMPORAL_MAX_PRECISION
13971401
+ ")";
13981402
} else {
13991403
return SSType.SMALLDATETIME.toString();
@@ -2159,6 +2163,8 @@ else if (null != sourceCryptoMeta) {
21592163
case java.sql.Types.TIME:
21602164
case java.sql.Types.TIMESTAMP:
21612165
case microsoft.sql.Types.DATETIMEOFFSET:
2166+
case microsoft.sql.Types.DATETIME:
2167+
case microsoft.sql.Types.SMALLDATETIME:
21622168
bulkJdbcType = java.sql.Types.VARCHAR;
21632169
break;
21642170
default:

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,16 +2489,19 @@ private void checkValidColumns(TypeInfo ti) throws SQLServerException {
24892489
String typeName;
24902490
MessageFormat form;
24912491
switch (jdbctype) {
2492+
case microsoft.sql.Types.DATETIME:
2493+
case microsoft.sql.Types.SMALLDATETIME:
24922494
case microsoft.sql.Types.MONEY:
24932495
case microsoft.sql.Types.SMALLMONEY:
24942496
case java.sql.Types.DATE:
2495-
case microsoft.sql.Types.DATETIME:
2496-
case microsoft.sql.Types.DATETIMEOFFSET:
2497-
case microsoft.sql.Types.SMALLDATETIME:
24982497
case java.sql.Types.TIME:
2498+
case microsoft.sql.Types.DATETIMEOFFSET:
24992499
typeName = ti.getSSTypeName();
2500-
form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupportedDW"));
2501-
throw new IllegalArgumentException(form.format(new Object[] {typeName}));
2500+
if (connection.isAzureDW()) {
2501+
// Azure DW does not support these data types.
2502+
form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupportedDW"));
2503+
throw new IllegalArgumentException(form.format(new Object[] { typeName }));
2504+
}
25022505
case java.sql.Types.INTEGER:
25032506
case java.sql.Types.SMALLINT:
25042507
case java.sql.Types.BIGINT:

src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
import java.sql.Statement;
1919
import java.sql.Time;
2020
import java.sql.Timestamp;
21+
import java.time.LocalDateTime;
22+
import java.time.LocalTime;
23+
import java.time.OffsetDateTime;
2124
import java.time.ZoneId;
25+
import java.time.ZoneOffset;
2226
import java.util.ArrayList;
2327
import java.util.Arrays;
2428
import java.util.Calendar;
@@ -832,6 +836,221 @@ public void testNoSpaceInsert() throws Exception {
832836
}
833837
}
834838

839+
/**
840+
* Test bulk insert with all temporal types and money as varchar when useBulkCopyForBatchInsert is true.
841+
* sendTemporalDataTypesAsStringForBulkCopy is set to true by default.
842+
* Temporal types are sent as varchar, and money/smallMoney are sent as their respective types.
843+
*
844+
* @throws Exception
845+
*/
846+
@Test
847+
@Tag(Constants.xAzureSQLDW)
848+
public void testBulkInsertWithAllTemporalTypesAndMoneyAsVarchar() throws Exception {
849+
String tableName = RandomUtil.getIdentifier("BulkTable");
850+
String createTableSQL = "CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (" +
851+
"dateTimeColumn DATETIME, " +
852+
"smallDateTimeColumn SMALLDATETIME, " +
853+
"dateTime2Column DATETIME2, " +
854+
"dateColumn DATE, " +
855+
"timeColumn TIME, " +
856+
"dateTimeOffsetColumn DATETIMEOFFSET, " +
857+
"moneyColumn MONEY, " +
858+
"smallMoneyColumn SMALLMONEY" + ")";
859+
String insertSQL = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) +
860+
" (dateTimeColumn, smallDateTimeColumn, dateTime2Column, dateColumn, timeColumn, dateTimeOffsetColumn, moneyColumn, smallMoneyColumn) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
861+
String selectSQL = "SELECT dateTimeColumn, smallDateTimeColumn, dateTime2Column, dateColumn, timeColumn, dateTimeOffsetColumn, moneyColumn, smallMoneyColumn FROM "
862+
+ AbstractSQLGenerator.escapeIdentifier(tableName);
863+
864+
try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;");
865+
Statement stmt = connection.createStatement();
866+
SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(insertSQL)) {
867+
868+
// Drop and create table
869+
TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt);
870+
stmt.execute(createTableSQL);
871+
872+
Timestamp dateTimeVal = Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 30, 45));
873+
String expectedDateTimeString = "2025-05-13 14:30:45.0";
874+
875+
Timestamp smallDateTimeVal = Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 30, 45));
876+
String expectedSmallDateTimeString = "2025-05-13 14:31:00.0";
877+
878+
Timestamp dateTime2Val = Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 30, 25, 123000000));
879+
String expectedDateTime2String = "2025-05-13 14:30:25.1230000";
880+
881+
Date dateVal = Date.valueOf("2025-06-02");
882+
String expectedDateString = "2025-06-02";
883+
884+
Time timeVal = Time.valueOf("14:30:00");
885+
String expectedTimeString = "14:30:00";
886+
887+
OffsetDateTime offsetDateTimeVal = OffsetDateTime.of(2025, 5, 13, 14, 30, 0, 0, ZoneOffset.UTC);
888+
DateTimeOffset dateTimeOffsetVal = DateTimeOffset.valueOf(offsetDateTimeVal);
889+
String expectedDateTimeOffsetString = "2025-05-13 14:30:00 +00:00";
890+
891+
BigDecimal moneyVal = new BigDecimal("12345.6789");
892+
String expectedMoneyString = "12345.6789";
893+
894+
BigDecimal smallMoneyVal = new BigDecimal("1234.5611");
895+
String expectedSmallMoneyString = "1234.5611";
896+
897+
pstmt.setTimestamp(1, dateTimeVal); // DATETIME
898+
pstmt.setSmallDateTime(2, smallDateTimeVal); // SMALLDATETIME
899+
pstmt.setObject(3, dateTime2Val); // DATETIME2
900+
pstmt.setDate(4, dateVal); // DATE
901+
pstmt.setObject(5, timeVal); // TIME
902+
pstmt.setDateTimeOffset(6, dateTimeOffsetVal); // DATETIMEOFFSET
903+
pstmt.setMoney(7, moneyVal); // MONEY
904+
pstmt.setSmallMoney(8, smallMoneyVal); // SMALLMONEY
905+
906+
pstmt.addBatch();
907+
pstmt.executeBatch();
908+
909+
// Validate inserted data
910+
try (ResultSet rs = stmt.executeQuery(selectSQL)) {
911+
assertTrue(rs.next());
912+
913+
assertEquals(dateTimeVal, rs.getTimestamp(1));
914+
assertEquals(expectedDateTimeString, rs.getString(1));
915+
916+
assertEquals(Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 31, 0)), rs.getTimestamp(2));
917+
assertEquals(expectedSmallDateTimeString, rs.getString(2));
918+
919+
assertEquals(dateTime2Val, rs.getTimestamp(3));
920+
assertEquals(expectedDateTime2String, rs.getString(3));
921+
922+
assertEquals(dateVal, rs.getDate(4));
923+
assertEquals(expectedDateString, rs.getString(4));
924+
925+
assertEquals(timeVal, rs.getObject(5));
926+
assertEquals(expectedTimeString, rs.getObject(5).toString());
927+
928+
assertEquals(dateTimeOffsetVal, rs.getObject(6, DateTimeOffset.class));
929+
assertEquals(expectedDateTimeOffsetString, rs.getObject(6).toString());
930+
931+
assertEquals(moneyVal, rs.getBigDecimal(7));
932+
assertEquals(expectedMoneyString, rs.getBigDecimal(7).toString());
933+
934+
assertEquals(smallMoneyVal, rs.getBigDecimal(8));
935+
assertEquals(expectedSmallMoneyString,rs.getBigDecimal(8).toString());
936+
937+
}
938+
} finally {
939+
try (Statement stmt = connection.createStatement()) {
940+
TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt);
941+
}
942+
}
943+
}
944+
945+
/**
946+
* Test bulk insert with all temporal types and money as varchar when useBulkCopyForBatchInsert is true.
947+
* and sendTemporalDataTypesAsStringForBulkCopy is set to false explicitly.
948+
* In this case all data types are sent as their respective types, including temporal types and money/smallMoney.
949+
*
950+
* @throws Exception
951+
*/
952+
@Test
953+
@Tag(Constants.xAzureSQLDW)
954+
public void testBulkInsertWithAllTemporalTypesAndMoney() throws Exception {
955+
String tableName = RandomUtil.getIdentifier("BulkTable");
956+
String createTableSQL = "CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (" +
957+
"dateTimeColumn DATETIME, " +
958+
"smallDateTimeColumn SMALLDATETIME, " +
959+
"dateTime2Column DATETIME2, " +
960+
"dateColumn DATE, " +
961+
"timeColumn TIME, " +
962+
"dateTimeOffsetColumn DATETIMEOFFSET, " +
963+
"moneyColumn MONEY, " +
964+
"smallMoneyColumn SMALLMONEY" + ")";
965+
String insertSQL = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) +
966+
" (dateTimeColumn, smallDateTimeColumn, dateTime2Column, dateColumn, timeColumn, dateTimeOffsetColumn, moneyColumn, smallMoneyColumn) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
967+
String selectSQL = "SELECT dateTimeColumn, smallDateTimeColumn, dateTime2Column, dateColumn, timeColumn, dateTimeOffsetColumn, moneyColumn, smallMoneyColumn FROM "
968+
+ AbstractSQLGenerator.escapeIdentifier(tableName);
969+
970+
try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;");
971+
Statement stmt = connection.createStatement();
972+
SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(insertSQL)) {
973+
974+
// Drop and create table
975+
TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt);
976+
stmt.execute(createTableSQL);
977+
978+
Timestamp dateTimeVal = Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 30, 45));
979+
String expectedDateTimeString = "2025-05-13 14:30:45.0";
980+
981+
Timestamp smallDateTimeVal = Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 30, 45));
982+
String expectedSmallDateTimeString = "2025-05-13 14:31:00.0";
983+
984+
Timestamp dateTime2Val = Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 30, 25, 123000000));
985+
String expectedDateTime2String = "2025-05-13 14:30:25.1230000";
986+
987+
Date dateVal = Date.valueOf("2025-06-02");
988+
String expectedDateString = "2025-06-02";
989+
990+
LocalTime time = LocalTime.of(14, 30, 0);
991+
Timestamp timeVal = Timestamp.valueOf(
992+
LocalDateTime.of(1970, 1, 1, time.getHour(), time.getMinute(), time.getSecond()));
993+
pstmt.setTimestamp(5, timeVal);
994+
String expectedTimeString = "14:30:00";
995+
996+
OffsetDateTime offsetDateTimeVal = OffsetDateTime.of(2025, 5, 13, 14, 30, 0, 0, ZoneOffset.UTC);
997+
DateTimeOffset dateTimeOffsetVal = DateTimeOffset.valueOf(offsetDateTimeVal);
998+
String expectedDateTimeOffsetString = "2025-05-13 14:30:00 +00:00";
999+
1000+
BigDecimal moneyVal = new BigDecimal("12345.6789");
1001+
String expectedMoneyString = "12345.6789";
1002+
1003+
BigDecimal smallMoneyVal = new BigDecimal("1234.5611");
1004+
String expectedSmallMoneyString = "1234.5611";
1005+
1006+
pstmt.setTimestamp(1, dateTimeVal); // DATETIME
1007+
pstmt.setSmallDateTime(2, smallDateTimeVal); // SMALLDATETIME
1008+
pstmt.setObject(3, dateTime2Val); // DATETIME2
1009+
pstmt.setDate(4, dateVal); // DATE
1010+
pstmt.setTimestamp(5, timeVal); // TIME
1011+
pstmt.setDateTimeOffset(6, dateTimeOffsetVal); // DATETIMEOFFSET
1012+
pstmt.setMoney(7, moneyVal); // MONEY
1013+
pstmt.setSmallMoney(8, smallMoneyVal); // SMALLMONEY
1014+
1015+
pstmt.addBatch();
1016+
pstmt.executeBatch();
1017+
1018+
// Validate inserted data
1019+
try (ResultSet rs = stmt.executeQuery(selectSQL)) {
1020+
assertTrue(rs.next());
1021+
1022+
assertEquals(dateTimeVal, rs.getTimestamp(1));
1023+
assertEquals(expectedDateTimeString, rs.getString(1));
1024+
1025+
assertEquals(Timestamp.valueOf(LocalDateTime.of(2025, 5, 13, 14, 31, 0)), rs.getTimestamp(2));
1026+
assertEquals(expectedSmallDateTimeString, rs.getString(2));
1027+
1028+
assertEquals(dateTime2Val, rs.getTimestamp(3));
1029+
assertEquals(expectedDateTime2String, rs.getString(3));
1030+
1031+
assertEquals(dateVal, rs.getDate(4));
1032+
assertEquals(expectedDateString, rs.getString(4));
1033+
1034+
assertEquals(Time.valueOf(time), rs.getObject(5));
1035+
assertEquals(expectedTimeString, rs.getObject(5).toString());
1036+
1037+
assertEquals(dateTimeOffsetVal, rs.getObject(6, DateTimeOffset.class));
1038+
assertEquals(expectedDateTimeOffsetString, rs.getObject(6).toString());
1039+
1040+
assertEquals(moneyVal, rs.getBigDecimal(7));
1041+
assertEquals(expectedMoneyString, rs.getBigDecimal(7).toString());
1042+
1043+
assertEquals(smallMoneyVal, rs.getBigDecimal(8));
1044+
assertEquals(expectedSmallMoneyString,rs.getBigDecimal(8).toString());
1045+
1046+
}
1047+
} finally {
1048+
try (Statement stmt = connection.createStatement()) {
1049+
TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt);
1050+
}
1051+
}
1052+
}
1053+
8351054
@BeforeAll
8361055
public static void setupTests() throws Exception {
8371056
setConnection();

0 commit comments

Comments
 (0)