Skip to content

Commit

Permalink
Feature | Extended bulk copy support for Azure DW (#1331)
Browse files Browse the repository at this point in the history
Feature | Extended bulk copy support for Azure DW
  • Loading branch information
peterbae authored May 22, 2020
1 parent 90bd0a7 commit f857e25
Show file tree
Hide file tree
Showing 16 changed files with 778 additions and 60 deletions.
27 changes: 26 additions & 1 deletion src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,30 @@ static final byte[] convertBigDecimalToBytes(BigDecimal bigDecimalVal, int scale
return valueBytes;
}

static final byte[] convertMoneyToBytes(BigDecimal bigDecimalVal, int bLength) {
byte[] valueBytes = new byte[bLength];

BigInteger bi = bigDecimalVal.unscaledValue();

if (bLength == 8) {
// money
byte[] longbArray = new byte[bLength];
Util.writeLong(bi.longValue(), longbArray, 0);
/*
* TDS 2.2.5.5.1.4 Fixed-Point Numbers
* Money is represented as a 8 byte signed integer, with one 4-byte integer that represents
* the more significant half, and one 4-byte integer that represents the less significant half.
*/
System.arraycopy(longbArray, 0, valueBytes, 4, 4);
System.arraycopy(longbArray, 4, valueBytes, 0, 4);
} else {
// smallmoney
Util.writeInt(bi.intValue(), valueBytes, 0);
}

return valueBytes;
}

/**
* Convert a BigDecimal object to desired target user type.
*
Expand Down Expand Up @@ -1289,7 +1313,8 @@ private static Object convertTemporalToObject(JDBCType jdbcType, SSType ssType,

case DATETIME2: {
return String.format(Locale.US, "%1$tF %1$tT%2$s", // yyyy-mm-dd hh:mm:ss[.nnnnnnn]
java.sql.Timestamp.valueOf(ldt), fractionalSecondsString(subSecondNanos, fractionalSecondsScale));
java.sql.Timestamp.valueOf(ldt),
fractionalSecondsString(subSecondNanos, fractionalSecondsScale));
}

case DATETIME: // and SMALLDATETIME
Expand Down
127 changes: 111 additions & 16 deletions src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -3397,6 +3397,28 @@ void writeBigDecimal(BigDecimal bigDecimalVal, int srcJdbcType, int precision,
writeBytes(bytes);
}

/**
* Append a money/smallmoney value in the TDS stream.
*
* @param moneyVal
* the money data value.
* @param srcJdbcType
* the source JDBCType
* @throws SQLServerException
*/
void writeMoney(BigDecimal moneyVal, int srcJdbcType) throws SQLServerException {
moneyVal = moneyVal.setScale(4, RoundingMode.HALF_UP);

int bLength;

// Money types are 8 bytes, smallmoney are 4 bytes
bLength = (srcJdbcType == microsoft.sql.Types.MONEY ? 8 : 4);
writeByte((byte) (bLength));

byte[] valueBytes = DDC.convertMoneyToBytes(moneyVal, bLength);
writeBytes(valueBytes);
}

/**
* Append a big decimal inside sql_variant in the TDS stream.
*
Expand Down Expand Up @@ -3574,27 +3596,100 @@ void writeTime(java.sql.Timestamp value, int scale) throws SQLServerException {
void writeDateTimeOffset(Object value, int scale, SSType destSSType) throws SQLServerException {
GregorianCalendar calendar;
TimeZone timeZone; // Time zone to associate with the value in the Gregorian calendar
long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos;
int minutesOffset;

microsoft.sql.DateTimeOffset dtoValue = (microsoft.sql.DateTimeOffset) value;
utcMillis = dtoValue.getTimestamp().getTime();
subSecondNanos = dtoValue.getTimestamp().getNanos();
minutesOffset = dtoValue.getMinutesOffset();
/*
* Out of all the supported temporal datatypes, DateTimeOffset is the only datatype that doesn't
* allow direct casting from java.sql.timestamp (which was created from a String).
* DateTimeOffset was never required to be constructed from a String, but with the
* introduction of extended bulk copy support for Azure DW, we now need to support this scenario.
* Parse the DTO as string if it's coming from a CSV.
*/
if (value instanceof String) {
// expected format: YYYY-MM-DD hh:mm:ss[.nnnnnnn] [{+|-}hh:mm]
try {
String stringValue = (String) value;
int lastColon = stringValue.lastIndexOf(':');

String offsetString = stringValue.substring(lastColon - 3);

// If the target data type is DATETIMEOFFSET, then use UTC for the calendar that
// will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
// Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
// use a local time zone determined by the minutes offset of the value, since
// the writers for those types expect local calendars.
timeZone = (SSType.DATETIMEOFFSET == destSSType) ? UTC.timeZone
: new SimpleTimeZone(minutesOffset * 60 * 1000, "");
/*
* At this point, offsetString should look like +hh:mm or -hh:mm. Otherwise, the optional offset
* value has not been provided. Parse accordingly.
*/
String timestampString;

calendar = new GregorianCalendar(timeZone, Locale.US);
calendar.setLenient(true);
calendar.clear();
calendar.setTimeInMillis(utcMillis);
if (!offsetString.startsWith("+") && !offsetString.startsWith("-")) {
minutesOffset = 0;
timestampString = stringValue;
} else {
minutesOffset = 60 * Integer.valueOf(offsetString.substring(1, 3))
+ Integer.valueOf(offsetString.substring(4, 6));
timestampString = stringValue.substring(0, lastColon - 4);

if (offsetString.startsWith("-"))
minutesOffset = -minutesOffset;
}

/*
* If the target data type is DATETIMEOFFSET, then use UTC for the calendar that
* will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
* Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
* use a local time zone determined by the minutes offset of the value, since
* the writers for those types expect local calendars.
*/
timeZone = (SSType.DATETIMEOFFSET == destSSType) ? UTC.timeZone
: new SimpleTimeZone(minutesOffset * 60 * 1000, "");

calendar = new GregorianCalendar(timeZone);

int year = Integer.valueOf(timestampString.substring(0, 4));
int month = Integer.valueOf(timestampString.substring(5, 7));
int day = Integer.valueOf(timestampString.substring(8, 10));
int hour = Integer.valueOf(timestampString.substring(11, 13));
int minute = Integer.valueOf(timestampString.substring(14, 16));
int second = Integer.valueOf(timestampString.substring(17, 19));

subSecondNanos = (19 == timestampString.indexOf('.')) ? (new BigDecimal(timestampString.substring(19)))
.scaleByPowerOfTen(9).intValue() : 0;

calendar.setLenient(true);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
calendar.add(Calendar.MINUTE, -minutesOffset);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ParsingDataError"));
Object[] msgArgs = {value, JDBCType.DATETIMEOFFSET};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
} else {
long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)

microsoft.sql.DateTimeOffset dtoValue = (microsoft.sql.DateTimeOffset) value;
utcMillis = dtoValue.getTimestamp().getTime();
subSecondNanos = dtoValue.getTimestamp().getNanos();
minutesOffset = dtoValue.getMinutesOffset();

/*
* If the target data type is DATETIMEOFFSET, then use UTC for the calendar that
* will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
* Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
* use a local time zone determined by the minutes offset of the value, since
* the writers for those types expect local calendars.
*/
timeZone = (SSType.DATETIMEOFFSET == destSSType) ? UTC.timeZone
: new SimpleTimeZone(minutesOffset * 60 * 1000, "");

calendar = new GregorianCalendar(timeZone, Locale.US);
calendar.setLenient(true);
calendar.clear();
calendar.setTimeInMillis(utcMillis);
}

writeScaledTemporal(calendar, subSecondNanos, scale, SSType.DATETIMEOFFSET);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -991,4 +991,19 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
*/
void setClientKeyPassword(String password);

/**
* Returns the current flag for value sendTemporalDataTypesAsStringForBulkCopy
*
* @return 'sendTemporalDataTypesAsStringForBulkCopy' property value.
*/
boolean getSendTemporalDataTypesAsStringForBulkCopy();

/**
* Specifies the flag to send temporal datatypes as String for Bulk Copy.
*
* @param sendTemporalDataTypesAsStringForBulkCopy
* boolean value for 'sendTemporalDataTypesAsStringForBulkCopy'.
*/
void setSendTemporalDataTypesAsStringForBulkCopy(boolean sendTemporalDataTypesAsStringForBulkCopy);

}
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ public Object[] getRowData() throws SQLServerException {
break;
}

case microsoft.sql.Types.MONEY:
case microsoft.sql.Types.SMALLMONEY:
case Types.DECIMAL:
case Types.NUMERIC: {
BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].trim());
Expand Down
Loading

0 comments on commit f857e25

Please sign in to comment.