From 249d401092afbaac1a3ffd370d846fb5ac5da511 Mon Sep 17 00:00:00 2001 From: Mahendra Chavan Date: Fri, 17 Jan 2025 20:57:11 +0530 Subject: [PATCH 1/7] Reusing DTVImpl objects and short circuting null checks --- .../com/microsoft/sqlserver/jdbc/Column.java | 4 +- .../microsoft/sqlserver/jdbc/IOBuffer.java | 22 ++++ .../com/microsoft/sqlserver/jdbc/dtv.java | 116 ++++++++++++++---- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java index 50958571d4..c1684c2e00 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java @@ -129,6 +129,7 @@ final SQLIdentifier getTableName() { this.baseColumnName = columnName; this.tableName = tableName; this.cryptoMetadata = cryptoMeta; + this.getterDTV.setImpl(new ServerDTVImpl()); } CryptoMetadata getCryptoMetadata() { @@ -139,7 +140,8 @@ CryptoMetadata getCryptoMetadata() { * Clears the values associated with this column. */ final void clear() { - getterDTV.clear(); + //getterDTV.clear(); + getterDTV.reset(); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index f51a5430c9..147f4271dc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -7000,6 +7000,20 @@ final TDSReaderMark mark() { return mark; } + final TDSPacket markPacket() { + isStreaming = false; + if (logger.isLoggable(Level.FINEST)) + logger.finest(this.toString() + ": Buffering from packet: " + currentPacket.toString()); + return currentPacket; + } + + final int markPacketOffset() { + isStreaming = false; + if (logger.isLoggable(Level.FINEST)) + logger.finest(this.toString() + ": Buffering from packet offset : " + payloadOffset); + return payloadOffset; + } + final void reset(TDSReaderMark mark) { if (logger.isLoggable(Level.FINEST)) logger.finest(this.toString() + ": Resetting to: " + mark.toString()); @@ -7008,6 +7022,14 @@ final void reset(TDSReaderMark mark) { payloadOffset = mark.payloadOffset; } + final void reset(TDSPacket markPacket, int markOffset) { + if (logger.isLoggable(Level.FINEST)) + logger.finest(this.toString() + ": Resetting to: " + markPacket.toString() + " : " + markOffset); + + currentPacket = markPacket; + payloadOffset = markOffset; + } + final void stream() { isStreaming = true; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 9ba2e83cea..2e548e4933 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -129,7 +129,7 @@ final class DTV { /** The source (app or server) providing the data for this value. */ private DTVImpl impl; - + CryptoMetadata cryptoMeta = null; JDBCType jdbcTypeSetByUser = null; int valueLength = 0; @@ -159,7 +159,8 @@ final void clear() { } final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException { - if (null == impl) + + if (null == impl) impl = new ServerDTVImpl(); impl.skipValue(type, tdsReader, isDiscard); @@ -169,9 +170,10 @@ final void initFromCompressedNull() { if (null == impl) impl = new ServerDTVImpl(); - impl.initFromCompressedNull(); + impl.initFromCompressedNull(); } + final void setStreamSetterArgs(StreamSetterArgs streamSetterArgs) { impl.setStreamSetterArgs(streamSetterArgs); } @@ -211,9 +213,9 @@ boolean isNull() { * @return true if impl is not null */ final boolean isInitialized() { - return (null != impl); + return ((null != impl) && impl.isInitialized()); } - + final void setJdbcType(JDBCType jdbcType) { if (null == impl) impl = new AppDTVImpl(); @@ -246,10 +248,12 @@ final JavaType getJavaType() { Object getValue(JDBCType jdbcType, int scale, InputStreamGetterArgs streamGetterArgs, Calendar cal, TypeInfo typeInfo, CryptoMetadata cryptoMetadata, TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException { - if (null == impl) + if (impl == null) { impl = new ServerDTVImpl(); - return impl.getValue(this, jdbcType, scale, streamGetterArgs, cal, typeInfo, cryptoMetadata, tdsReader, - statement); + } else if (impl.isNull()) { + return null; + } + return impl.getValue(this, jdbcType, scale, streamGetterArgs, cal, typeInfo, cryptoMetadata, tdsReader,statement); } Object getSetterValue() { @@ -1918,6 +1922,10 @@ void sendByRPC(String name, TypeInfo typeInfo, SQLCollation collation, int preci // typeInfo is null when called from PreparedStatement->Parameter->SendByRPC executeOp(new SendByRPCOp(name, typeInfo, collation, precision, outScale, isOutParam, tdsWriter, statement)); } + + public void reset() { + impl.reset(); + } } @@ -1932,7 +1940,13 @@ abstract void setValue(DTV dtv, SQLCollation collation, JDBCType jdbcType, Objec StreamSetterArgs streamSetterArgs, Calendar cal, Integer scale, SQLServerConnection con, boolean forceEncrypt) throws SQLServerException; - abstract void setValue(Object value, JavaType javaType); + protected abstract boolean isInitialized(); + + protected abstract void reset(); + + protected abstract void setInitialized(); + + abstract void setValue(Object value, JavaType javaType); abstract void setStreamSetterArgs(StreamSetterArgs streamSetterArgs); @@ -2366,6 +2380,22 @@ SqlVariant getInternalVariant() { void setInternalVariant(SqlVariant type) { this.internalVariant = type; } + + + @Override + protected boolean isInitialized() { + return true; + } + + @Override + protected void setInitialized() { + //NoOp + } + + @Override + protected void reset() { + //NoOp + } } @@ -3242,12 +3272,16 @@ static TypeInfo getInstance(TDSReader tdsReader, boolean readFlags) throws SQLSe * DTV implementation for values set from the TDS response stream. */ final class ServerDTVImpl extends DTVImpl { + private int valueLength; - private TDSReaderMark valueMark; + //private TDSReaderMark valueMark; + private TDSPacket valueMarkPacket; + private int valueMarkOffset; private boolean isNull; private SqlVariant internalVariant; + private boolean isInitialized = false; - /** + /** * Sets the value of the DTV to an app-specified Java type. * * Generally, the value cannot be stored back into the TDS byte stream (although this could be done for fixed-length @@ -3271,7 +3305,9 @@ void setValue(Object value, JavaType javaType) { // whole value of the stream has been consumed. // Note this only to be used by the streams returned to the user. void setPositionAfterStreamed(TDSReader tdsReader) { - valueMark = tdsReader.mark(); + //valueMark = tdsReader.mark(); + valueMarkPacket = tdsReader.markPacket(); + valueMarkOffset = tdsReader.markPacketOffset(); valueLength = STREAMCONSUMED; } @@ -3338,20 +3374,26 @@ JavaType getJavaType() { // for the DTV when a null value is // received from NBCROW for a particular column final void initFromCompressedNull() { - assert valueMark == null; + //assert valueMark == null; + assert valueMarkPacket == null; isNull = true; + isInitialized = true; } final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException { // indicates that this value was obtained from NBCROW // So, there is nothing else to read from the wire - if (null == valueMark && isNull) { + //if (null == valueMark && isNull) { + if (null == valueMarkPacket && isNull) { return; } - if (null == valueMark) + //if (null == valueMark) + if (null == valueMarkPacket) getValuePrep(type, tdsReader); - tdsReader.reset(valueMark); + + //tdsReader.reset(valueMark); + tdsReader.reset(valueMarkPacket, valueMarkOffset); // value length zero means that the stream has been already skipped to the end - adaptive case if (valueLength != STREAMCONSUMED) { if (valueLength == DataTypes.UNKNOWN_STREAM_LENGTH) { @@ -3378,8 +3420,8 @@ final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) thro private void getValuePrep(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException { // If we've already seen this value before, then we shouldn't be here. - assert null == valueMark; - + //assert null == valueMark; + assert null == valueMarkPacket; // Otherwise, mark the value's location, figure out its length, and determine whether it was NULL. switch (typeInfo.getSSLenType()) { case PARTLENTYPE: @@ -3428,7 +3470,9 @@ else if (SSType.SQL_VARIANT == typeInfo.getSSType()) { if (valueLength > typeInfo.getMaxLength()) tdsReader.throwInvalidTDS(); - valueMark = tdsReader.mark(); + //valueMark = tdsReader.mark(); + valueMarkPacket = tdsReader.markPacket(); + valueMarkOffset = tdsReader.markPacketOffset(); } Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo baseTypeInfo, SQLServerConnection con, @@ -3674,12 +3718,14 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str // If valueMark == null and isNull, it implies that // the column is null according to NBCROW and that // there is nothing to be read from the wire. - if (null == valueMark && (!isNull)) + //if (null == valueMark && (!isNull)) + if (null == valueMarkPacket && (!isNull)) getValuePrep(typeInfo, tdsReader); // either there should be a valueMark // or valueMark should be null and isNull should be set to true(NBCROW case) - assert ((valueMark != null) || (valueMark == null && isNull)); + //assert ((valueMark != null) || (valueMark == null && isNull)); + assert ((valueMarkPacket != null) || (valueMarkPacket == null && isNull)); if (null != streamGetterArgs) { if (!streamGetterArgs.streamType.convertsFrom(typeInfo)) @@ -3706,8 +3752,8 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str } if (!isNull) { - tdsReader.reset(valueMark); - + //tdsReader.reset(valueMark); + tdsReader.reset(valueMarkPacket, valueMarkOffset); if (encrypted) { if (DataTypes.UNKNOWN_STREAM_LENGTH == valueLength) { convertedValue = DDC.convertStreamToObject( @@ -3888,9 +3934,10 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str // Postcondition: returned object is null only if value was null. assert isNull || null != convertedValue; + setInitialized(); return convertedValue; } - + SqlVariant getInternalVariant() { return internalVariant; } @@ -4154,4 +4201,25 @@ private int getDaysIntoCE(byte[] datePortion, SSType baseSSType) throws SQLServe return daysIntoCE; } + + @Override + protected boolean isInitialized() { + return this.isInitialized; + } + + @Override + protected void setInitialized() { + this.isInitialized = true; + } + + @Override + protected void reset() { + this.isNull = false; + //this.valueMark = null; + this.valueMarkPacket = null; + this.valueMarkOffset = 0; + this.valueLength = 0; + this.internalVariant = null; + this.isInitialized = false; + } } From 0204305b1085ac0b30226a0da134e70ef68c33d7 Mon Sep 17 00:00:00 2001 From: Mahendra Chavan Date: Sun, 2 Feb 2025 09:37:45 +0530 Subject: [PATCH 2/7] Row prefetching experiment --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 88 +++++++++++++-- .../sqlserver/jdbc/SQLServerConnection.java | 2 +- .../jdbc/SQLServerPrefetchedResultSet.java | 105 ++++++++++++++++++ .../sqlserver/jdbc/SQLServerResultSet.java | 19 ++-- .../sqlserver/jdbc/SQLServerStatement.java | 11 +- 5 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 147f4271dc..9bd3cc6453 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -58,6 +58,8 @@ import java.util.SimpleTimeZone; import java.util.TimeZone; import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -629,6 +631,10 @@ final TDSReader getReader(TDSCommand command) { return new TDSReader(this, con, command); } + final TDSReader getRowReader(TDSCommand command) { + return new TDSRowReader(this, con, command); + } + // Socket for raw TCP/IP communications with SQL Server private transient Socket tcpSocket; @@ -6665,7 +6671,7 @@ void sendEnclavePackage(String sql, ArrayList enclaveCEKs) throws SQLSer */ final class TDSPacket { final byte[] header = new byte[TDS.PACKET_HEADER_SIZE]; - final byte[] payload; + byte[] payload; int payloadLength; volatile TDSPacket next; @@ -6679,6 +6685,14 @@ final public String toString() { payloadLength = 0; next = null; } + + public void reset(int size) { + if (payload.length < size) { + payload = new byte[size]; + } + payloadLength = 0; + next = null; + } final boolean isEOM() { return TDS.STATUS_BIT_EOM == (header[TDS.PACKET_HEADER_MESSAGE_STATUS] & TDS.STATUS_BIT_EOM); @@ -6703,13 +6717,35 @@ final class TDSReaderMark { } } +final class TDSPacketPool { + private static int INITIAL_SIZE = 2; + private final BlockingQueue packetQueue = new LinkedBlockingDeque(); + TDSPacketPool() { + for (int i = 0; i < INITIAL_SIZE; ++i) { + packetQueue.add(new TDSPacket(0)); + } + } + + TDSPacket get(int packetSize) { + if (packetQueue.isEmpty()) { + packetQueue.add(new TDSPacket(packetSize)); + } + TDSPacket p = packetQueue.poll(); + p.reset(packetSize); + return p; + } + + void release(TDSPacket p) { + packetQueue.add(p); + } +} /** * TDSReader encapsulates the TDS response data stream. * * Bytes are read from SQL Server into a FIFO of packets. Reader methods traverse the packets to access the data. */ -final class TDSReader implements Serializable { +class TDSReader implements Serializable { /** * Always update serialVersionUID when prompted. @@ -6738,9 +6774,9 @@ final SQLServerConnection getConnection() { return con; } - private transient TDSPacket currentPacket = new TDSPacket(0); + protected transient TDSPacket currentPacket = new TDSPacket(0); private transient TDSPacket lastPacket = currentPacket; - private int payloadOffset = 0; + protected int payloadOffset = 0; private int packetNum = 0; private boolean isStreaming = true; @@ -6824,7 +6860,7 @@ private boolean ensurePayload() throws SQLServerException { * * @return true if additional data is available to be read false if no more data is available */ - private boolean nextPacket() throws SQLServerException { + protected boolean nextPacket() throws SQLServerException { assert null != currentPacket; // Shouldn't call this function unless we're at the end of the current packet... @@ -6879,7 +6915,8 @@ final boolean readPacket() throws SQLServerException { assert tdsChannel.numMsgsRcvd < tdsChannel.numMsgsSent : "numMsgsRcvd:" + tdsChannel.numMsgsRcvd + " should be less than numMsgsSent:" + tdsChannel.numMsgsSent; - TDSPacket newPacket = new TDSPacket(con.getTDSPacketSize()); + TDSPacket newPacket = newPacket(con.getTDSPacketSize()); + //TDSPacket newPacket = tdsPacketPool.get(con.getTDSPacketSize()); if ((null != command) && // if cancelQueryTimeout is set, we should wait for the total amount of // queryTimeout + cancelQueryTimeout to @@ -6963,8 +7000,7 @@ final boolean readPacket() throws SQLServerException { ++packetNum; - lastPacket.next = newPacket; - lastPacket = newPacket; + linkPackets(newPacket); // When logging, append the payload to the log buffer and write out the whole thing. if (tdsChannel.isLoggingPackets() && logBuffer != null) { @@ -6990,6 +7026,15 @@ final boolean readPacket() throws SQLServerException { } } + protected void linkPackets(TDSPacket newPacket) { + lastPacket.next = newPacket; + lastPacket = newPacket; + } + + protected TDSPacket newPacket(int tdsPacketSize) { + return new TDSPacket(tdsPacketSize); + } + final TDSReaderMark mark() { TDSReaderMark mark = new TDSReaderMark(currentPacket, payloadOffset); isStreaming = false; @@ -7558,6 +7603,33 @@ final void trySetSensitivityClassification(SensitivityClassification sensitivity } } +class TDSRowReader extends TDSReader { + + TDSRowReader(TDSChannel tdsChannel, SQLServerConnection con, TDSCommand command) { + super(tdsChannel, con, command); + } + + @Override + protected TDSPacket newPacket(int tdsPacketSize) { + currentPacket.reset(tdsPacketSize); + return currentPacket; + } + + @Override + protected boolean nextPacket() throws SQLServerException { + boolean r = readPacket(); + if (r) { + payloadOffset = 0; + } + return r; + } + + @Override + protected void linkPackets(TDSPacket newPacket) { + // + } + +} /** * TDSCommand encapsulates an interruptable TDS conversation. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 5f076f9af6..4cc43d4337 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4474,7 +4474,7 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException { */ boolean commandComplete = false; try { - commandComplete = newCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(newCommand)); + commandComplete = newCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(newCommand)); } finally { /* * If execution of the new command left response bytes on the wire (e.g. a large ResultSet or complex diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java new file mode 100644 index 0000000000..933933aa0b --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java @@ -0,0 +1,105 @@ +package com.microsoft.sqlserver.jdbc; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; + +public class SQLServerPrefetchedResultSet extends SQLServerResultSet { + + private static final BigInteger MIN_VALUE_LONG_BI = new BigInteger(String.valueOf(Long.MIN_VALUE)); + private static final BigInteger MAX_VALUE_LONG_BI = new BigInteger(String.valueOf(Long.MAX_VALUE)); + private static final BigDecimal MIN_VALUE_LONG_BD = new BigDecimal(String.valueOf(Long.MIN_VALUE)); + private static final BigDecimal MAX_VALUE_LONG_BD = new BigDecimal(String.valueOf(Long.MAX_VALUE)); + + private Object[] row = null; + + SQLServerPrefetchedResultSet(SQLServerStatement stmtIn) throws SQLServerException { + super(stmtIn); + this.row = new Object[this.columns.length]; + } + + private void loadRow() throws SQLServerException { + initializeNullCompressedColumns(); + // Iterate on columns and get values and keep them in row + for (int index = 1; index <= this.columns.length; ++index) { + Column c = getColumn(index); + // Column c = columns[index - 1]; + Object o = c.getValue(c.getTypeInfo().getSSType().getJDBCType(), null, null, tdsReader, stmt); + row[index - 1] = o; + } + } + + @Override + public boolean next() throws SQLServerException { + super.checkClosed(); + boolean r = super.next(); + if (r) { + loadRow(); + } + return r; + } + + @Override + protected void checkClosed() throws SQLServerException { + // Do nothing + } + + @Override + protected Object getValue(int columnIndex, JDBCType jdbcType, InputStreamGetterArgs getterArgs, Calendar cal) + throws SQLServerException { + Object o = row[columnIndex - 1]; + lastValueWasNull = (null == o); + return o; + } + + //TODO + //implement methods like below for all data types to convert data appropriately and return. + + @Override + public long getLong(int columnIndex) throws SQLServerException { + loggerExternal.entering(getClassNameLogging(), "getLong", columnIndex); + checkClosed(); + Object o = getValue(columnIndex, JDBCType.BIGINT); + if (o == null) { + return 0; + } + if (o instanceof BigDecimal) { + BigDecimal val = (BigDecimal) o; + if (val.compareTo(MIN_VALUE_LONG_BD) < 0 || val.compareTo(MAX_VALUE_LONG_BD) > 0) { + // TODO: change this to standard exception + throw new SQLServerException("Numeric Overflow", null); + } else { + return new Long(val.longValue()); + } + } else if (o instanceof Long) { + return (Long) o; + } else if (o instanceof Boolean) { + return ((Boolean) o).booleanValue() ? 1L : 0L; + } else if (o instanceof Byte) { + return new Long(((Byte) o).byteValue() & 0xFF); + } else if (o instanceof BigInteger) { + BigInteger val = (BigInteger) o; + if (val.compareTo(MIN_VALUE_LONG_BI) < 0 || val.compareTo(MAX_VALUE_LONG_BI) > 0) { + // TODO: change this to standard exception + throw new SQLServerException("Numeric Overflow", null); + } else { + return val.longValue(); + } + } else if (o instanceof Number) { + return new Long(((Number) o).longValue()); + } else if (o instanceof String) { + return new Long(((String) o).trim()); + } + loggerExternal.exiting(getClassNameLogging(), "getLong", o); + // return null != o ? Long.valueOf(o.toString()) : 0; + return 0; + } + +/* + @Override + protected void skipColumns(int columnsToSkip, boolean discardValues) throws SQLServerException { + // Do nothing + //super.skipColumns(columnsToSkip, discardValues); + } +*/ +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java index fff271b2a2..d384b9063f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java @@ -50,6 +50,9 @@ enum RowType { */ public class SQLServerResultSet implements ISQLServerResultSet, java.io.Serializable { + //TODO make it configurable/adaptive + public static boolean prefetch = true; + /** * Always refresh SerialVersionUID when prompted */ @@ -95,7 +98,7 @@ String getClassNameLogging() { } /** the statement that generated this result set */ - private final SQLServerStatement stmt; + protected final SQLServerStatement stmt; /** max rows to return from this result set */ private final int maxRows; @@ -128,7 +131,7 @@ protected int getServerCursorId() { private boolean isOnInsertRow = false; /** true if the last value read was SQL NULL */ - private boolean lastValueWasNull = false; + protected boolean lastValueWasNull = false; /** The index (1-based) of the last column in the current row that has been marked for reading */ private int lastColumnIndex; @@ -212,7 +215,7 @@ final void setDeletedCurrentRow(boolean rowDeleted) { private int rowCount; /** The current row's column values */ - private final transient Column[] columns; + protected final transient Column[] columns; /** The CekTable retrieved from the COLMETADATA token for this resultset */ private CekTable cekTable = null; @@ -230,7 +233,7 @@ final void setColumnName(int index, String name) { * Skips columns between the last marked column and the target column, inclusive, optionally discarding their values * as they are skipped. */ - private void skipColumns(int columnsToSkip, boolean discardValues) throws SQLServerException { + protected void skipColumns(int columnsToSkip, boolean discardValues) throws SQLServerException { assert lastColumnIndex >= 1; assert 0 <= columnsToSkip && columnsToSkip <= columns.length; @@ -243,7 +246,7 @@ private void skipColumns(int columnsToSkip, boolean discardValues) throws SQLSer } /** TDS reader from which row values are read */ - private TDSReader tdsReader; + protected TDSReader tdsReader; TDSReader getTDSReader() { return tdsReader; @@ -781,7 +784,7 @@ final Column getColumn(int columnIndex) throws SQLServerException { * * @throws SQLServerException */ - private void initializeNullCompressedColumns() throws SQLServerException { + protected void initializeNullCompressedColumns() throws SQLServerException { if (resultSetCurrentRowType.equals(RowType.NBCROW) && (!areNullCompressedColumnsInitialized)) { int columnNo = 0; // no of bytes to be read from the stream @@ -2087,7 +2090,7 @@ Column getterGetColumn(int index) throws SQLServerException { return loadColumn(index); } - private Object getValue(int columnIndex, JDBCType jdbcType) throws SQLServerException { + protected Object getValue(int columnIndex, JDBCType jdbcType) throws SQLServerException { return getValue(columnIndex, jdbcType, null, null); } @@ -2100,7 +2103,7 @@ private Object getValue(int columnIndex, JDBCType jdbcType, return getValue(columnIndex, jdbcType, getterArgs, null); } - private Object getValue(int columnIndex, JDBCType jdbcType, InputStreamGetterArgs getterArgs, + protected Object getValue(int columnIndex, JDBCType jdbcType, InputStreamGetterArgs getterArgs, Calendar cal) throws SQLServerException { Object o = getterGetColumn(columnIndex).getValue(jdbcType, getterArgs, cal, tdsReader, stmt); lastValueWasNull = (null == o); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index ab9e9fbe2b..b0d148dc4f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -1788,7 +1788,7 @@ boolean onInfo(TDSReader tdsReader) throws SQLServerException { // Not an error. Is it a result set? else if (nextResult.isResultSet()) { - resultSet = new SQLServerResultSet(this); + resultSet = createResultSet(); return true; } @@ -2616,6 +2616,15 @@ SQLServerColumnEncryptionKeyStoreProvider getColumnEncryptionKeyStoreProvider( lock.unlock(); } } + + private final SQLServerResultSet createResultSet() throws SQLServerException { + if (SQLServerResultSet.prefetch) { + return new SQLServerPrefetchedResultSet(this); + } + return new SQLServerResultSet(this); + + } + } From 98bd95663fba955cacec3f99c5fa53d0f2bcafbf Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Wed, 27 Aug 2025 14:24:22 +0530 Subject: [PATCH 3/7] Reverted TDSRowReader implementation and DTV packet marking changes to use TDSReaderMark. --- .../com/microsoft/sqlserver/jdbc/Column.java | 3 +- .../microsoft/sqlserver/jdbc/IOBuffer.java | 102 ++---------------- .../sqlserver/jdbc/SQLServerConnection.java | 2 +- .../sqlserver/jdbc/SQLServerResultSet.java | 2 +- .../sqlserver/jdbc/SQLServerStatement.java | 2 - .../com/microsoft/sqlserver/jdbc/dtv.java | 40 +++---- 6 files changed, 21 insertions(+), 130 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java index c1684c2e00..be01958d5d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java @@ -140,8 +140,7 @@ CryptoMetadata getCryptoMetadata() { * Clears the values associated with this column. */ final void clear() { - //getterDTV.clear(); - getterDTV.reset(); + getterDTV.clear(); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 9bd3cc6453..776dfddc48 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -631,10 +631,6 @@ final TDSReader getReader(TDSCommand command) { return new TDSReader(this, con, command); } - final TDSReader getRowReader(TDSCommand command) { - return new TDSRowReader(this, con, command); - } - // Socket for raw TCP/IP communications with SQL Server private transient Socket tcpSocket; @@ -6671,7 +6667,7 @@ void sendEnclavePackage(String sql, ArrayList enclaveCEKs) throws SQLSer */ final class TDSPacket { final byte[] header = new byte[TDS.PACKET_HEADER_SIZE]; - byte[] payload; + final byte[] payload; int payloadLength; volatile TDSPacket next; @@ -6685,14 +6681,6 @@ final public String toString() { payloadLength = 0; next = null; } - - public void reset(int size) { - if (payload.length < size) { - payload = new byte[size]; - } - payloadLength = 0; - next = null; - } final boolean isEOM() { return TDS.STATUS_BIT_EOM == (header[TDS.PACKET_HEADER_MESSAGE_STATUS] & TDS.STATUS_BIT_EOM); @@ -6717,35 +6705,12 @@ final class TDSReaderMark { } } -final class TDSPacketPool { - private static int INITIAL_SIZE = 2; - private final BlockingQueue packetQueue = new LinkedBlockingDeque(); - TDSPacketPool() { - for (int i = 0; i < INITIAL_SIZE; ++i) { - packetQueue.add(new TDSPacket(0)); - } - } - - TDSPacket get(int packetSize) { - if (packetQueue.isEmpty()) { - packetQueue.add(new TDSPacket(packetSize)); - } - TDSPacket p = packetQueue.poll(); - p.reset(packetSize); - return p; - } - - void release(TDSPacket p) { - packetQueue.add(p); - } -} - /** * TDSReader encapsulates the TDS response data stream. * * Bytes are read from SQL Server into a FIFO of packets. Reader methods traverse the packets to access the data. */ -class TDSReader implements Serializable { +final class TDSReader implements Serializable { /** * Always update serialVersionUID when prompted. @@ -6774,9 +6739,9 @@ final SQLServerConnection getConnection() { return con; } - protected transient TDSPacket currentPacket = new TDSPacket(0); + private transient TDSPacket currentPacket = new TDSPacket(0); private transient TDSPacket lastPacket = currentPacket; - protected int payloadOffset = 0; + private int payloadOffset = 0; private int packetNum = 0; private boolean isStreaming = true; @@ -6860,7 +6825,7 @@ private boolean ensurePayload() throws SQLServerException { * * @return true if additional data is available to be read false if no more data is available */ - protected boolean nextPacket() throws SQLServerException { + private boolean nextPacket() throws SQLServerException { assert null != currentPacket; // Shouldn't call this function unless we're at the end of the current packet... @@ -6915,8 +6880,7 @@ final boolean readPacket() throws SQLServerException { assert tdsChannel.numMsgsRcvd < tdsChannel.numMsgsSent : "numMsgsRcvd:" + tdsChannel.numMsgsRcvd + " should be less than numMsgsSent:" + tdsChannel.numMsgsSent; - TDSPacket newPacket = newPacket(con.getTDSPacketSize()); - //TDSPacket newPacket = tdsPacketPool.get(con.getTDSPacketSize()); + TDSPacket newPacket = new TDSPacket(con.getTDSPacketSize()); if ((null != command) && // if cancelQueryTimeout is set, we should wait for the total amount of // queryTimeout + cancelQueryTimeout to @@ -7031,10 +6995,6 @@ protected void linkPackets(TDSPacket newPacket) { lastPacket = newPacket; } - protected TDSPacket newPacket(int tdsPacketSize) { - return new TDSPacket(tdsPacketSize); - } - final TDSReaderMark mark() { TDSReaderMark mark = new TDSReaderMark(currentPacket, payloadOffset); isStreaming = false; @@ -7045,20 +7005,6 @@ final TDSReaderMark mark() { return mark; } - final TDSPacket markPacket() { - isStreaming = false; - if (logger.isLoggable(Level.FINEST)) - logger.finest(this.toString() + ": Buffering from packet: " + currentPacket.toString()); - return currentPacket; - } - - final int markPacketOffset() { - isStreaming = false; - if (logger.isLoggable(Level.FINEST)) - logger.finest(this.toString() + ": Buffering from packet offset : " + payloadOffset); - return payloadOffset; - } - final void reset(TDSReaderMark mark) { if (logger.isLoggable(Level.FINEST)) logger.finest(this.toString() + ": Resetting to: " + mark.toString()); @@ -7067,14 +7013,6 @@ final void reset(TDSReaderMark mark) { payloadOffset = mark.payloadOffset; } - final void reset(TDSPacket markPacket, int markOffset) { - if (logger.isLoggable(Level.FINEST)) - logger.finest(this.toString() + ": Resetting to: " + markPacket.toString() + " : " + markOffset); - - currentPacket = markPacket; - payloadOffset = markOffset; - } - final void stream() { isStreaming = true; } @@ -7603,34 +7541,6 @@ final void trySetSensitivityClassification(SensitivityClassification sensitivity } } -class TDSRowReader extends TDSReader { - - TDSRowReader(TDSChannel tdsChannel, SQLServerConnection con, TDSCommand command) { - super(tdsChannel, con, command); - } - - @Override - protected TDSPacket newPacket(int tdsPacketSize) { - currentPacket.reset(tdsPacketSize); - return currentPacket; - } - - @Override - protected boolean nextPacket() throws SQLServerException { - boolean r = readPacket(); - if (r) { - payloadOffset = 0; - } - return r; - } - - @Override - protected void linkPackets(TDSPacket newPacket) { - // - } - -} - /** * TDSCommand encapsulates an interruptable TDS conversation. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 4cc43d4337..5f076f9af6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4474,7 +4474,7 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException { */ boolean commandComplete = false; try { - commandComplete = newCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(newCommand)); + commandComplete = newCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(newCommand)); } finally { /* * If execution of the new command left response bytes on the wire (e.g. a large ResultSet or complex diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java index d384b9063f..e63fab0ea3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java @@ -233,7 +233,7 @@ final void setColumnName(int index, String name) { * Skips columns between the last marked column and the target column, inclusive, optionally discarding their values * as they are skipped. */ - protected void skipColumns(int columnsToSkip, boolean discardValues) throws SQLServerException { + private void skipColumns(int columnsToSkip, boolean discardValues) throws SQLServerException { assert lastColumnIndex >= 1; assert 0 <= columnsToSkip && columnsToSkip <= columns.length; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index b0d148dc4f..de393c31db 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -2622,9 +2622,7 @@ private final SQLServerResultSet createResultSet() throws SQLServerException { return new SQLServerPrefetchedResultSet(this); } return new SQLServerResultSet(this); - } - } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 2e548e4933..8f92e65dd6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -3274,9 +3274,7 @@ static TypeInfo getInstance(TDSReader tdsReader, boolean readFlags) throws SQLSe final class ServerDTVImpl extends DTVImpl { private int valueLength; - //private TDSReaderMark valueMark; - private TDSPacket valueMarkPacket; - private int valueMarkOffset; + private TDSReaderMark valueMark; private boolean isNull; private SqlVariant internalVariant; private boolean isInitialized = false; @@ -3305,9 +3303,7 @@ void setValue(Object value, JavaType javaType) { // whole value of the stream has been consumed. // Note this only to be used by the streams returned to the user. void setPositionAfterStreamed(TDSReader tdsReader) { - //valueMark = tdsReader.mark(); - valueMarkPacket = tdsReader.markPacket(); - valueMarkOffset = tdsReader.markPacketOffset(); + valueMark = tdsReader.mark(); valueLength = STREAMCONSUMED; } @@ -3374,8 +3370,7 @@ JavaType getJavaType() { // for the DTV when a null value is // received from NBCROW for a particular column final void initFromCompressedNull() { - //assert valueMark == null; - assert valueMarkPacket == null; + assert valueMark == null; isNull = true; isInitialized = true; } @@ -3383,17 +3378,14 @@ final void initFromCompressedNull() { final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException { // indicates that this value was obtained from NBCROW // So, there is nothing else to read from the wire - //if (null == valueMark && isNull) { - if (null == valueMarkPacket && isNull) { + if (null == valueMark && isNull) { return; } - //if (null == valueMark) - if (null == valueMarkPacket) + if (null == valueMark) getValuePrep(type, tdsReader); - //tdsReader.reset(valueMark); - tdsReader.reset(valueMarkPacket, valueMarkOffset); + tdsReader.reset(valueMark); // value length zero means that the stream has been already skipped to the end - adaptive case if (valueLength != STREAMCONSUMED) { if (valueLength == DataTypes.UNKNOWN_STREAM_LENGTH) { @@ -3420,8 +3412,7 @@ final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) thro private void getValuePrep(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException { // If we've already seen this value before, then we shouldn't be here. - //assert null == valueMark; - assert null == valueMarkPacket; + assert null == valueMark; // Otherwise, mark the value's location, figure out its length, and determine whether it was NULL. switch (typeInfo.getSSLenType()) { case PARTLENTYPE: @@ -3470,9 +3461,7 @@ else if (SSType.SQL_VARIANT == typeInfo.getSSType()) { if (valueLength > typeInfo.getMaxLength()) tdsReader.throwInvalidTDS(); - //valueMark = tdsReader.mark(); - valueMarkPacket = tdsReader.markPacket(); - valueMarkOffset = tdsReader.markPacketOffset(); + valueMark = tdsReader.mark(); } Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo baseTypeInfo, SQLServerConnection con, @@ -3718,14 +3707,12 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str // If valueMark == null and isNull, it implies that // the column is null according to NBCROW and that // there is nothing to be read from the wire. - //if (null == valueMark && (!isNull)) - if (null == valueMarkPacket && (!isNull)) + if (null == valueMark && (!isNull)) getValuePrep(typeInfo, tdsReader); // either there should be a valueMark // or valueMark should be null and isNull should be set to true(NBCROW case) - //assert ((valueMark != null) || (valueMark == null && isNull)); - assert ((valueMarkPacket != null) || (valueMarkPacket == null && isNull)); + assert ((valueMark != null) || (valueMark == null && isNull)); if (null != streamGetterArgs) { if (!streamGetterArgs.streamType.convertsFrom(typeInfo)) @@ -3752,8 +3739,7 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str } if (!isNull) { - //tdsReader.reset(valueMark); - tdsReader.reset(valueMarkPacket, valueMarkOffset); + tdsReader.reset(valueMark); if (encrypted) { if (DataTypes.UNKNOWN_STREAM_LENGTH == valueLength) { convertedValue = DDC.convertStreamToObject( @@ -4215,9 +4201,7 @@ protected void setInitialized() { @Override protected void reset() { this.isNull = false; - //this.valueMark = null; - this.valueMarkPacket = null; - this.valueMarkOffset = 0; + this.valueMark = null; this.valueLength = 0; this.internalVariant = null; this.isInitialized = false; From 3f46be7e6d02805053e8449790378f37333365a4 Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Wed, 27 Aug 2025 15:00:34 +0530 Subject: [PATCH 4/7] Removed isInitialized tracking and reset() methods from DTV implementation --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 3 +- .../com/microsoft/sqlserver/jdbc/dtv.java | 67 +++---------------- 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 776dfddc48..a17e3d28c3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -58,8 +58,6 @@ import java.util.SimpleTimeZone; import java.util.TimeZone; import java.util.UUID; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -7541,6 +7539,7 @@ final void trySetSensitivityClassification(SensitivityClassification sensitivity } } + /** * TDSCommand encapsulates an interruptable TDS conversation. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 8f92e65dd6..74f6f96c02 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -129,7 +129,6 @@ final class DTV { /** The source (app or server) providing the data for this value. */ private DTVImpl impl; - CryptoMetadata cryptoMeta = null; JDBCType jdbcTypeSetByUser = null; int valueLength = 0; @@ -159,8 +158,7 @@ final void clear() { } final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException { - - if (null == impl) + if (null == impl) impl = new ServerDTVImpl(); impl.skipValue(type, tdsReader, isDiscard); @@ -170,10 +168,9 @@ final void initFromCompressedNull() { if (null == impl) impl = new ServerDTVImpl(); - impl.initFromCompressedNull(); + impl.initFromCompressedNull(); } - final void setStreamSetterArgs(StreamSetterArgs streamSetterArgs) { impl.setStreamSetterArgs(streamSetterArgs); } @@ -213,9 +210,9 @@ boolean isNull() { * @return true if impl is not null */ final boolean isInitialized() { - return ((null != impl) && impl.isInitialized()); + return (null != impl); } - + final void setJdbcType(JDBCType jdbcType) { if (null == impl) impl = new AppDTVImpl(); @@ -1922,10 +1919,6 @@ void sendByRPC(String name, TypeInfo typeInfo, SQLCollation collation, int preci // typeInfo is null when called from PreparedStatement->Parameter->SendByRPC executeOp(new SendByRPCOp(name, typeInfo, collation, precision, outScale, isOutParam, tdsWriter, statement)); } - - public void reset() { - impl.reset(); - } } @@ -1940,13 +1933,7 @@ abstract void setValue(DTV dtv, SQLCollation collation, JDBCType jdbcType, Objec StreamSetterArgs streamSetterArgs, Calendar cal, Integer scale, SQLServerConnection con, boolean forceEncrypt) throws SQLServerException; - protected abstract boolean isInitialized(); - - protected abstract void reset(); - - protected abstract void setInitialized(); - - abstract void setValue(Object value, JavaType javaType); + abstract void setValue(Object value, JavaType javaType); abstract void setStreamSetterArgs(StreamSetterArgs streamSetterArgs); @@ -2380,22 +2367,6 @@ SqlVariant getInternalVariant() { void setInternalVariant(SqlVariant type) { this.internalVariant = type; } - - - @Override - protected boolean isInitialized() { - return true; - } - - @Override - protected void setInitialized() { - //NoOp - } - - @Override - protected void reset() { - //NoOp - } } @@ -3372,7 +3343,6 @@ JavaType getJavaType() { final void initFromCompressedNull() { assert valueMark == null; isNull = true; - isInitialized = true; } final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException { @@ -3384,8 +3354,7 @@ final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) thro if (null == valueMark) getValuePrep(type, tdsReader); - - tdsReader.reset(valueMark); + tdsReader.reset(valueMark); // value length zero means that the stream has been already skipped to the end - adaptive case if (valueLength != STREAMCONSUMED) { if (valueLength == DataTypes.UNKNOWN_STREAM_LENGTH) { @@ -3413,6 +3382,7 @@ final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) thro private void getValuePrep(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException { // If we've already seen this value before, then we shouldn't be here. assert null == valueMark; + // Otherwise, mark the value's location, figure out its length, and determine whether it was NULL. switch (typeInfo.getSSLenType()) { case PARTLENTYPE: @@ -3740,6 +3710,7 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str if (!isNull) { tdsReader.reset(valueMark); + if (encrypted) { if (DataTypes.UNKNOWN_STREAM_LENGTH == valueLength) { convertedValue = DDC.convertStreamToObject( @@ -3920,10 +3891,9 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str // Postcondition: returned object is null only if value was null. assert isNull || null != convertedValue; - setInitialized(); return convertedValue; } - + SqlVariant getInternalVariant() { return internalVariant; } @@ -4187,23 +4157,4 @@ private int getDaysIntoCE(byte[] datePortion, SSType baseSSType) throws SQLServe return daysIntoCE; } - - @Override - protected boolean isInitialized() { - return this.isInitialized; - } - - @Override - protected void setInitialized() { - this.isInitialized = true; - } - - @Override - protected void reset() { - this.isNull = false; - this.valueMark = null; - this.valueLength = 0; - this.internalVariant = null; - this.isInitialized = false; - } } From a6e12205b2167b8dad213d9f833156dd8a84a860 Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Wed, 27 Aug 2025 15:14:01 +0530 Subject: [PATCH 5/7] Clean up TDS reader implementation and code formatting --- src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java | 9 +++------ src/main/java/com/microsoft/sqlserver/jdbc/dtv.java | 7 +++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index a17e3d28c3..f51a5430c9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -6703,6 +6703,7 @@ final class TDSReaderMark { } } + /** * TDSReader encapsulates the TDS response data stream. * @@ -6962,7 +6963,8 @@ final boolean readPacket() throws SQLServerException { ++packetNum; - linkPackets(newPacket); + lastPacket.next = newPacket; + lastPacket = newPacket; // When logging, append the payload to the log buffer and write out the whole thing. if (tdsChannel.isLoggingPackets() && logBuffer != null) { @@ -6988,11 +6990,6 @@ final boolean readPacket() throws SQLServerException { } } - protected void linkPackets(TDSPacket newPacket) { - lastPacket.next = newPacket; - lastPacket = newPacket; - } - final TDSReaderMark mark() { TDSReaderMark mark = new TDSReaderMark(currentPacket, payloadOffset); isStreaming = false; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 74f6f96c02..f560bcbe4d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -129,6 +129,7 @@ final class DTV { /** The source (app or server) providing the data for this value. */ private DTVImpl impl; + CryptoMetadata cryptoMeta = null; JDBCType jdbcTypeSetByUser = null; int valueLength = 0; @@ -3243,14 +3244,12 @@ static TypeInfo getInstance(TDSReader tdsReader, boolean readFlags) throws SQLSe * DTV implementation for values set from the TDS response stream. */ final class ServerDTVImpl extends DTVImpl { - private int valueLength; private TDSReaderMark valueMark; private boolean isNull; private SqlVariant internalVariant; - private boolean isInitialized = false; - /** + /** * Sets the value of the DTV to an app-specified Java type. * * Generally, the value cannot be stored back into the TDS byte stream (although this could be done for fixed-length @@ -3274,7 +3273,7 @@ void setValue(Object value, JavaType javaType) { // whole value of the stream has been consumed. // Note this only to be used by the streams returned to the user. void setPositionAfterStreamed(TDSReader tdsReader) { - valueMark = tdsReader.mark(); + valueMark = tdsReader.mark(); valueLength = STREAMCONSUMED; } From e3ca5360aa005db60fe32ac6199a2f903b336fb5 Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Tue, 2 Sep 2025 11:43:47 +0530 Subject: [PATCH 6/7] Implement JTDS-style result set prefetching with comprehensive data type conversions --- .../jdbc/SQLServerPrefetchedResultSet.java | 760 ++++++++++++++++-- .../sqlserver/jdbc/SQLServerResultSet.java | 2 +- 2 files changed, 701 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java index 933933aa0b..c6e0282199 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java @@ -1,16 +1,14 @@ package com.microsoft.sqlserver.jdbc; +import java.io.ByteArrayInputStream; import java.math.BigDecimal; -import java.math.BigInteger; +import java.nio.charset.Charset; +import java.sql.SQLException; import java.util.Calendar; +import java.util.Locale; +import java.util.UUID; public class SQLServerPrefetchedResultSet extends SQLServerResultSet { - - private static final BigInteger MIN_VALUE_LONG_BI = new BigInteger(String.valueOf(Long.MIN_VALUE)); - private static final BigInteger MAX_VALUE_LONG_BI = new BigInteger(String.valueOf(Long.MAX_VALUE)); - private static final BigDecimal MIN_VALUE_LONG_BD = new BigDecimal(String.valueOf(Long.MIN_VALUE)); - private static final BigDecimal MAX_VALUE_LONG_BD = new BigDecimal(String.valueOf(Long.MAX_VALUE)); - private Object[] row = null; SQLServerPrefetchedResultSet(SQLServerStatement stmtIn) throws SQLServerException { @@ -23,12 +21,26 @@ private void loadRow() throws SQLServerException { // Iterate on columns and get values and keep them in row for (int index = 1; index <= this.columns.length; ++index) { Column c = getColumn(index); - // Column c = columns[index - 1]; - Object o = c.getValue(c.getTypeInfo().getSSType().getJDBCType(), null, null, tdsReader, stmt); + SSType ssType = c.getTypeInfo().getSSType(); + + // For temporal types, get as TIMESTAMP to preserve fractional seconds + // except for DATETIMEOFFSET which needs to preserve timezone information + JDBCType jdbcType = (ssType == SSType.TIME || ssType == SSType.DATETIME2) + ? JDBCType.TIMESTAMP + : ssType.getJDBCType(); + + // Get the value from the TDS reader + Object o = c.getValue(jdbcType, null, null, tdsReader, stmt); row[index - 1] = o; } } + /** + * Moves the cursor to the next row in the result set. + * + * @return true if the cursor is moved to a valid row, false if there are no more rows + * @throws SQLServerException if a database access error occurs + */ @Override public boolean next() throws SQLServerException { super.checkClosed(); @@ -39,67 +51,695 @@ public boolean next() throws SQLServerException { return r; } + /** + * Updates the current row with the values in the row cache. + * + * @throws SQLException if a database access error occurs + */ + @Override + public void updateRow() throws SQLException { + super.updateRow(); + super.doRefreshRow(); // Refresh the row cache + // Update the cached row with the latest values from the columns + loadRow(); + } + + /** + * Updates the current row with the values in the row cache. + * + * @throws SQLException if a database access error occurs + */ + @Override + public void updateString(int columnIndex, String stringValue) throws SQLServerException { + super.updateString(columnIndex, stringValue); + row[columnIndex - 1] = stringValue; + } + + /** + * Moves the cursor to the specified row in the result set. + * + * @param row the row number to move to (1-based) + * @return true if the cursor is moved to a valid row, false if the row is invalid + * @throws SQLException if a database access error occurs + */ + @Override + public boolean absolute(int row) throws SQLException { + boolean r = super.absolute(row); + if (r) { + loadRow(); + } + return r; + } + + /** + * Moves the cursor to the previous row in the result set. + * + * @return true if the cursor is moved to a valid row, false if there are no more rows + * @throws SQLException if a database access error occurs + */ + @Override + public boolean previous() throws SQLException { + boolean r = super.previous(); + if (r) { + loadRow(); + } + return r; + } + + /** + * Moves the cursor to the first row in the result set. + * + * @return true if the cursor is moved to a valid row, false if there are no rows + * @throws SQLException if a database access error occurs + */ @Override - protected void checkClosed() throws SQLServerException { - // Do nothing + public boolean first() throws SQLException { + boolean r = super.first(); + if (r) { + loadRow(); + } + return r; } + /** + * Moves the cursor to the specified row in the result set. + * + * @param row the row number to move to (1-based) + * @return true if the cursor is moved to a valid row, false if the row is invalid + * @throws SQLException if a database access error occurs + */ + @Override + public boolean relative(int rows) throws SQLException { + boolean r = super.relative(rows); + if (r) { + loadRow(); + } + return r; + } + + @Override + public java.io.InputStream getBinaryStream(int columnIndex) throws SQLServerException { + checkClosed(); + Object rawValue = row[columnIndex - 1]; + lastValueWasNull = (null == rawValue); + + if (rawValue == null) { + return null; + } + + if (rawValue instanceof byte[]) { + return new java.io.ByteArrayInputStream((byte[]) rawValue); + } else if (rawValue instanceof String) { + // Convert hex string to bytes + String hexStr = (String) rawValue; + if (hexStr.length() % 2 != 0) { + throw new SQLServerException("Invalid hex string length", null); + } + byte[] bytes = new byte[hexStr.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) Integer.parseInt(hexStr.substring(2 * i, 2 * i + 2), 16); + } + return new java.io.ByteArrayInputStream(bytes); + } else { + throw new SQLServerException("Cannot convert " + rawValue.getClass().getSimpleName() + " to InputStream", null); + } + } + + @Override + public java.io.InputStream getBinaryStream(String columnName) throws SQLServerException { + return getBinaryStream(findColumn(columnName)); + } + + @Override + public java.io.Reader getCharacterStream(int columnIndex) throws SQLServerException { + checkClosed(); + Object rawValue = row[columnIndex - 1]; + lastValueWasNull = (null == rawValue); + + if (rawValue == null) { + return null; + } + + if (rawValue instanceof String) { + return new java.io.StringReader((String) rawValue); + } else { + // Convert to string first + String strValue = rawValue.toString(); + return new java.io.StringReader(strValue); + } + } + + @Override + public java.io.Reader getCharacterStream(String columnName) throws SQLServerException { + return getCharacterStream(findColumn(columnName)); + } + + /** + * Retrieves the value for the specified column index. + * + * @param columnIndex the column index (1-based) + * @param jdbcType the JDBC type to convert to + * @param getterArgs additional arguments for the getter + * @param cal the calendar to use for date/time conversions + * @return the value for the specified column index + * @throws SQLServerException if a database access error occurs + */ @Override protected Object getValue(int columnIndex, JDBCType jdbcType, InputStreamGetterArgs getterArgs, Calendar cal) throws SQLServerException { - Object o = row[columnIndex - 1]; - lastValueWasNull = (null == o); - return o; + Object rawValue = row[columnIndex - 1]; + lastValueWasNull = (null == rawValue); + + if (rawValue == null) { + return null; + } + + Column c = getColumn(columnIndex); + SSType columnSSType = c.getTypeInfo().getSSType(); + + // Special handling for temporal types that we store as TIMESTAMP for precision + if ((columnSSType == SSType.TIME || columnSSType == SSType.DATETIME2) && + jdbcType == columnSSType.getJDBCType()) { + // For TIME and DATETIME2 stored as Timestamp, convert back to the expected type + if (columnSSType == SSType.TIME && jdbcType == JDBCType.TIME && rawValue instanceof java.sql.Timestamp) { + return convertTemporalUsingDDC(rawValue, columnSSType, jdbcType, cal, c); + } else if (columnSSType == SSType.DATETIME2 && jdbcType == JDBCType.TIMESTAMP) { + return rawValue; // Already a Timestamp + } + } + + // If requesting same type as stored, return directly + if (jdbcType == columnSSType.getJDBCType()) { + return rawValue; + } + + if (!columnSSType.convertsTo(jdbcType)) { + DataTypes.throwConversionError(columnSSType.toString(), jdbcType.toString()); + } + + // Otherwise, use DDC to convert + return convertUsingDDC(rawValue, jdbcType, columnSSType, getterArgs, cal, c); + } + + private Object convertUsingDDC(Object rawValue, JDBCType targetJdbcType, SSType sourceSSType, + InputStreamGetterArgs getterArgs, Calendar cal, Column column) + throws SQLServerException { + + StreamType streamType = (getterArgs != null) ? getterArgs.streamType : StreamType.NONE; + TypeInfo typeInfo = column.getTypeInfo(); + + try { + switch (sourceSSType) { + case VARBINARYMAX: + case VARCHARMAX: + case NVARCHARMAX: + case GEOMETRY: + case GEOGRAPHY: + case UDT: { + if (sourceSSType == SSType.VARCHARMAX && targetJdbcType == JDBCType.CLOB) { + try { + java.sql.Clob clob = stmt.connection.createClob(); + String stringValue = (String) rawValue; + if (!stringValue.isEmpty()) { + clob.setString(1, stringValue); + } + return clob; + } catch (SQLException e) { + throw new SQLServerException("Failed to create CLOB from VARCHARMAX", e); + } + } + + // When NVARCHARMAX columns are accessed via getNClob() or similar methods + if (sourceSSType == SSType.NVARCHARMAX && targetJdbcType == JDBCType.NCLOB) { + try { + java.sql.NClob nclob = stmt.connection.createNClob(); + String stringValue = (String) rawValue; + if (!stringValue.isEmpty()) { + nclob.setString(1, stringValue); + } + return nclob; + } catch (SQLException e) { + throw new SQLServerException("Failed to create NCLOB from NVARCHARMAX", e); + } + } + + if (sourceSSType.category == SSType.Category.UDT) { + if (targetJdbcType == JDBCType.GEOMETRY) { + if (!typeInfo.getSSTypeName().equalsIgnoreCase(targetJdbcType.toString())) { + DataTypes.throwConversionError(typeInfo.getSSTypeName().toUpperCase(), targetJdbcType.toString()); + } + return new Geometry((byte[]) rawValue); + } else if (targetJdbcType == JDBCType.GEOGRAPHY) { + if (!typeInfo.getSSTypeName().equalsIgnoreCase(targetJdbcType.toString())) { + DataTypes.throwConversionError(typeInfo.getSSTypeName().toUpperCase(), targetJdbcType.toString()); + } + return new Geography((byte[]) rawValue); + } + } + + // Handle BLOB conversion for VARBINARYMAX and other binary types + if (targetJdbcType == JDBCType.BLOB) { + // Use the deprecated constructor but it works for our case + @SuppressWarnings("deprecation") + SQLServerBlob blob = new SQLServerBlob(stmt.connection, (byte[]) rawValue); + return blob; + } + + if (rawValue instanceof byte[]) { + return DDC.convertBytesToObject((byte[]) rawValue, targetJdbcType, typeInfo); + } else { + Charset charset = (column.getTypeInfo().getSQLCollation() != null) ? column.getTypeInfo().getSQLCollation().getCharset() : null; + return DDC.convertStringToObject((String) rawValue, charset, targetJdbcType, streamType); + } + + } + + case XML: { + if (rawValue instanceof String) { + return DDC.convertStringToObject((String) rawValue, null, targetJdbcType, streamType); + } else if (rawValue instanceof byte[]) { + return DDC.convertBytesToObject((byte[]) rawValue, targetJdbcType, typeInfo); + } else { + return DDC.convertStringToObject(rawValue.toString(), null, targetJdbcType, streamType); + } + } + + // Convert other variable length native types + // (CHAR/VARCHAR/TEXT/NCHAR/NVARCHAR/NTEXT/BINARY/VARBINARY/IMAGE) -> ANY jdbcType. + case CHAR: + case VARCHAR: + case TEXT: + case NCHAR: + case NVARCHAR: + case NTEXT: + case IMAGE: + case BINARY: + case VARBINARY: + case TIMESTAMP: // A special BINARY(8) + { + if (rawValue instanceof String) { + Charset charset = (typeInfo.getSQLCollation() != null) + ? typeInfo.getSQLCollation().getCharset() + : null; + return DDC.convertStringToObject((String) rawValue, charset, targetJdbcType, streamType); + } else if (rawValue instanceof byte[]) { + return DDC.convertBytesToObject((byte[]) rawValue, targetJdbcType, typeInfo); + } else { + return DDC.convertStringToObject(rawValue.toString(), null, targetJdbcType, streamType); + } + } + + // Convert BIT/TINYINT/SMALLINT/INTEGER/BIGINT native type -> ANY jdbcType. + case BIT: + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: { + int valueLength = getValueLengthForInteger(sourceSSType); + switch (valueLength) { + case 8: + return DDC.convertLongToObject((Long) rawValue, targetJdbcType, sourceSSType, streamType); + + case 4: + return DDC.convertIntegerToObject((Integer) rawValue, valueLength, targetJdbcType, streamType); + + case 2: + return DDC.convertIntegerToObject((Short) rawValue, valueLength, targetJdbcType, streamType); + + case 1: + return DDC.convertIntegerToObject(((Boolean) rawValue) ? 1 : 0, 1, targetJdbcType, streamType); + + default: + assert false : "Unexpected valueLength" + valueLength; + throw new SQLServerException("jTDS Unexpected SSType " + sourceSSType, null, 0, null); + } + } + + // Convert DECIMAL|NUMERIC native types -> ANY jdbcType. + case DECIMAL: + case NUMERIC: + return DDC.convertBigDecimalToObject((BigDecimal) rawValue, targetJdbcType, streamType); + + // Convert MONEY|SMALLMONEY native types -> ANY jdbcType. + case MONEY: + case SMALLMONEY: + int precision = (sourceSSType == SSType.MONEY) ? 8 : 4; + return DDC.convertMoneyToObject((BigDecimal) rawValue, targetJdbcType, streamType, precision); + + // Convert FLOAT native type -> ANY jdbcType. + case FLOAT: + if (rawValue instanceof Double) { + return DDC.convertDoubleToObject((Double) rawValue, targetJdbcType, streamType); + } else { + return DDC.convertFloatToObject((Float) rawValue, targetJdbcType, streamType); + } + + + // Convert REAL native type -> ANY jdbcType. + case REAL: + return DDC.convertFloatToObject((Float) rawValue, targetJdbcType, streamType); + + // Convert DATETIME|SMALLDATETIME native types -> ANY jdbcType. + case DATETIME: + case SMALLDATETIME: + return convertTemporalUsingDDC(rawValue, sourceSSType, targetJdbcType, cal, column); + + // Convert DATE native type -> ANY jdbcType. + case DATE: + return convertTemporalUsingDDC(rawValue, sourceSSType, targetJdbcType, cal, column); + // Convert TIME native type -> ANY jdbcType. + case TIME: + return convertTemporalUsingDDC(rawValue, sourceSSType, targetJdbcType, cal, column); + + // Convert DATETIME2 native type -> ANY jdbcType. + case DATETIME2: + return convertTemporalUsingDDC(rawValue, sourceSSType, targetJdbcType, cal, column); + + // Convert DATETIMEOFFSET native type -> ANY jdbcType. + case DATETIMEOFFSET: + return convertTemporalUsingDDC(rawValue, sourceSSType, targetJdbcType, cal, column); + + // Convert GUID native type -> ANY jdbcType. + case GUID: + switch (targetJdbcType) { + case BINARY: + case VARBINARY: + case LONGVARBINARY:{ + return Util.asGuidByteArray(UUID.fromString(rawValue.toString())); + } + default: + if (StreamType.BINARY == streamType || StreamType.ASCII == streamType) + return new ByteArrayInputStream(rawValue.toString().getBytes(Encoding.UNICODE.charset())); + return rawValue; + } + + case SQL_VARIANT: + int valueLength = getValueLengthForSQLVariant(rawValue); + switch (valueLength) { + case 8: + return DDC.convertLongToObject((Long) rawValue, targetJdbcType, sourceSSType, streamType); + + case 4: + return DDC.convertIntegerToObject((Integer) rawValue, valueLength, targetJdbcType, streamType); + + case 2: + return DDC.convertIntegerToObject((Short) rawValue, valueLength, targetJdbcType, streamType); + + case 1: + return DDC.convertIntegerToObject(((Boolean) rawValue) ? 1 : 0, 1, targetJdbcType, streamType); + + case -1: + if (rawValue instanceof String) { + return DDC.convertStringToObject((String) rawValue, null, targetJdbcType, streamType); + } else if (rawValue instanceof byte[]) { + return DDC.convertBytesToObject((byte[]) rawValue, targetJdbcType, typeInfo); + } else { + return DDC.convertStringToObject(rawValue.toString(), null, targetJdbcType, streamType); + } + } + default: + assert false : "Unexpected SSType " + sourceSSType; + throw new SQLServerException("jTDS Unexpected SSType " + sourceSSType, null, 0, null); + + } + } catch (java.io.UnsupportedEncodingException e) { + throw new SQLServerException("Error converting string with charset", e); + } + } + + private int getValueLengthForInteger(SSType ssType) { + switch (ssType) { + case BIT: + return 1; + case SMALLINT: + case TINYINT: + return 2; + case INTEGER: + return 4; + case BIGINT: + return 8; + default: + return 4;//TO DO : error? + } } - //TODO - //implement methods like below for all data types to convert data appropriately and return. + private int getValueLengthForSQLVariant(Object rawValue) { + if (rawValue instanceof Long) { + return 8; + } else if (rawValue instanceof Integer) { + return 4; + } else if (rawValue instanceof Short) { + return 2; + } else if (rawValue instanceof Byte || rawValue instanceof Boolean) { + return 1; + } + return -1; + } - @Override - public long getLong(int columnIndex) throws SQLServerException { - loggerExternal.entering(getClassNameLogging(), "getLong", columnIndex); - checkClosed(); - Object o = getValue(columnIndex, JDBCType.BIGINT); - if (o == null) { - return 0; - } - if (o instanceof BigDecimal) { - BigDecimal val = (BigDecimal) o; - if (val.compareTo(MIN_VALUE_LONG_BD) < 0 || val.compareTo(MAX_VALUE_LONG_BD) > 0) { - // TODO: change this to standard exception - throw new SQLServerException("Numeric Overflow", null); - } else { - return new Long(val.longValue()); + private Object convertTemporalUsingDDC(Object temporalValue, SSType sourceSSType, JDBCType targetJdbcType, Calendar cal, Column column) throws SQLServerException { + switch (targetJdbcType) { + case TIMESTAMP: + if (temporalValue instanceof java.sql.Timestamp) { + return temporalValue; + } else if (temporalValue instanceof java.sql.Date) { + return new java.sql.Timestamp(((java.sql.Date) temporalValue).getTime()); + } else if (temporalValue instanceof java.sql.Time) { + return new java.sql.Timestamp(((java.sql.Time) temporalValue).getTime()); + } else if (temporalValue instanceof microsoft.sql.DateTimeOffset) { + return ((microsoft.sql.DateTimeOffset) temporalValue).getTimestamp(); + } + break; + + case DATE: + if (temporalValue instanceof java.sql.Date) { + return temporalValue; + } else if (temporalValue instanceof java.sql.Time) { + // TIME to DATE conversion - return epoch date (1970-01-01) + return new java.sql.Date(0); + } else if (temporalValue instanceof java.sql.Timestamp) { + java.util.GregorianCalendar tempCal = new java.util.GregorianCalendar(); + tempCal.setTime((java.util.Date) temporalValue); + tempCal.set(java.util.Calendar.HOUR_OF_DAY, 0); + tempCal.set(java.util.Calendar.MINUTE, 0); + tempCal.set(java.util.Calendar.SECOND, 0); + tempCal.set(java.util.Calendar.MILLISECOND, 0); + return new java.sql.Date(tempCal.getTime().getTime()); + } else if (temporalValue instanceof microsoft.sql.DateTimeOffset) { + microsoft.sql.DateTimeOffset dto = (microsoft.sql.DateTimeOffset) temporalValue; + java.util.GregorianCalendar tempCal = new java.util.GregorianCalendar(); + tempCal.setTime(dto.getTimestamp()); + tempCal.set(java.util.Calendar.HOUR_OF_DAY, 0); + tempCal.set(java.util.Calendar.MINUTE, 0); + tempCal.set(java.util.Calendar.SECOND, 0); + tempCal.set(java.util.Calendar.MILLISECOND, 0); + return new java.sql.Date(tempCal.getTime().getTime()); + } + break; + + case TIME: + if (temporalValue instanceof java.sql.Time) { + return temporalValue; + } else if (temporalValue instanceof java.sql.Date) { + // DATE to TIME conversion - return midnight time + return new java.sql.Time(0); + } else if (temporalValue instanceof java.sql.Timestamp) { + // Convert Timestamp to Time with proper rounding, following DDC logic + java.sql.Timestamp ts = (java.sql.Timestamp) temporalValue; + int subSecondNanos = ts.getNanos(); + + // Follow DDC rounding logic: round if fractional nanos >= 0.5 milliseconds + if (subSecondNanos % 1_000_000 >= 500_000) { // Nanos.PER_MILLISECOND / 2 + // Add 1 millisecond for rounding + long roundedTime = ts.getTime() + 1; + java.sql.Timestamp roundedTs = new java.sql.Timestamp(roundedTime); + java.util.Calendar tempCal = java.util.Calendar.getInstance(); + tempCal.setTime(roundedTs); + + // Set date to epoch (1970-01-01) and keep only time + tempCal.set(java.util.Calendar.YEAR, 1970); + tempCal.set(java.util.Calendar.MONTH, 0); + tempCal.set(java.util.Calendar.DAY_OF_MONTH, 1); + + return new java.sql.Time(tempCal.getTimeInMillis()); + } else { + // No rounding needed + java.util.Calendar tempCal = java.util.Calendar.getInstance(); + tempCal.setTime(ts); + + // Set date to epoch (1970-01-01) and keep only time + tempCal.set(java.util.Calendar.YEAR, 1970); + tempCal.set(java.util.Calendar.MONTH, 0); + tempCal.set(java.util.Calendar.DAY_OF_MONTH, 1); + + return new java.sql.Time(tempCal.getTimeInMillis()); + } + } else if (temporalValue instanceof microsoft.sql.DateTimeOffset) { + microsoft.sql.DateTimeOffset dto = (microsoft.sql.DateTimeOffset) temporalValue; + java.sql.Timestamp ts = dto.getTimestamp(); + int subSecondNanos = ts.getNanos(); + + // Follow DDC rounding logic for DateTimeOffset to Time conversion too + if (subSecondNanos % 1_000_000 >= 500_000) { // Nanos.PER_MILLISECOND / 2 + // Add 1 millisecond for rounding + long roundedTime = ts.getTime() + 1; + java.sql.Timestamp roundedTs = new java.sql.Timestamp(roundedTime); + java.util.Calendar tempCal2 = java.util.Calendar.getInstance(); + tempCal2.setTime(roundedTs); + + // Set date to epoch (1970-01-01) and keep only time + tempCal2.set(java.util.Calendar.YEAR, 1970); + tempCal2.set(java.util.Calendar.MONTH, 0); + tempCal2.set(java.util.Calendar.DAY_OF_MONTH, 1); + + return new java.sql.Time(tempCal2.getTimeInMillis()); + } else { + // No rounding needed + java.util.Calendar tempCal2 = java.util.Calendar.getInstance(); + tempCal2.setTime(ts); + + // Set date to epoch (1970-01-01) and keep only time + tempCal2.set(java.util.Calendar.YEAR, 1970); + tempCal2.set(java.util.Calendar.MONTH, 0); + tempCal2.set(java.util.Calendar.DAY_OF_MONTH, 1); + + return new java.sql.Time(tempCal2.getTimeInMillis()); + } + } + break; + + case LOCALDATETIME: { + if (temporalValue instanceof java.sql.Timestamp) { + // Direct conversion without timezone adjustment for timestamp-related types + java.sql.Timestamp ts = (java.sql.Timestamp) temporalValue; + java.util.Calendar tempCal = java.util.Calendar.getInstance(); + tempCal.setTime(ts); + return java.time.LocalDateTime.of( + tempCal.get(java.util.Calendar.YEAR), + tempCal.get(java.util.Calendar.MONTH) + 1, + tempCal.get(java.util.Calendar.DAY_OF_MONTH), + tempCal.get(java.util.Calendar.HOUR_OF_DAY), + tempCal.get(java.util.Calendar.MINUTE), + tempCal.get(java.util.Calendar.SECOND), + ts.getNanos() + ); + } else if (temporalValue instanceof microsoft.sql.DateTimeOffset) { + // For DateTimeOffset, check the connection setting for timezone behavior + microsoft.sql.DateTimeOffset dto = (microsoft.sql.DateTimeOffset) temporalValue; + if (stmt != null && stmt.connection != null && stmt.connection.getIgnoreOffsetOnDateTimeOffsetConversion()) { + // Ignore offset - use the original time components without timezone conversion + return dto.getOffsetDateTime().toLocalDateTime(); + } else { + // Apply offset conversion - convert to system local timezone + return dto.getOffsetDateTime().atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime(); + } + } else if (temporalValue instanceof java.sql.Date) { + java.sql.Timestamp ts = new java.sql.Timestamp(((java.sql.Date) temporalValue).getTime()); + return ts.toLocalDateTime(); + } else if (temporalValue instanceof java.sql.Time) { + java.sql.Timestamp ts = new java.sql.Timestamp(((java.sql.Time) temporalValue).getTime()); + return ts.toLocalDateTime(); + } + break; } - } else if (o instanceof Long) { - return (Long) o; - } else if (o instanceof Boolean) { - return ((Boolean) o).booleanValue() ? 1L : 0L; - } else if (o instanceof Byte) { - return new Long(((Byte) o).byteValue() & 0xFF); - } else if (o instanceof BigInteger) { - BigInteger val = (BigInteger) o; - if (val.compareTo(MIN_VALUE_LONG_BI) < 0 || val.compareTo(MAX_VALUE_LONG_BI) > 0) { - // TODO: change this to standard exception - throw new SQLServerException("Numeric Overflow", null); + + case VARCHAR: + case NVARCHAR: + case LONGVARCHAR: + case LONGNVARCHAR: + case CHAR: + case NCHAR: + return formatTemporalAsString(temporalValue, sourceSSType, column); + } + + return temporalValue.toString(); + } + + /** + * Format temporal values as strings with proper precision for DATETIME2 and other high-precision types. + */ + private String formatTemporalAsString(Object temporalValue, SSType sourceSSType, Column column) { + if (temporalValue instanceof java.sql.Timestamp) { + java.sql.Timestamp ts = (java.sql.Timestamp) temporalValue; + int subSecondNanos = ts.getNanos(); + + // Get the actual scale from the column, default to 7 if not available + int scale = (column != null) ? column.getTypeInfo().getScale() : 7; + + switch (sourceSSType) { + case DATETIME2: + return String.format(Locale.US, "%1$tF %1$tT%2$s", ts, fractionalSecondsString(subSecondNanos, scale)); + case TIME: + return String.format(Locale.US, "%1$tT%2$s", ts, fractionalSecondsString(subSecondNanos, scale)); + case DATETIMEOFFSET: + return String.format(Locale.US, "%1$tF %1$tT%2$s", ts, fractionalSecondsString(subSecondNanos, scale)); + case DATETIME: + case SMALLDATETIME: + return ts.toString(); + default: + return ts.toString(); + } + } else if (temporalValue instanceof java.sql.Date) { + return String.format(Locale.US, "%1$tF", (java.sql.Date) temporalValue); + } else if (temporalValue instanceof java.sql.Time) { + return String.format(Locale.US, "%1$tT", (java.sql.Time) temporalValue); + } else if (temporalValue instanceof microsoft.sql.DateTimeOffset) { + microsoft.sql.DateTimeOffset dto = (microsoft.sql.DateTimeOffset) temporalValue; + // Get the actual scale from the column, default to 7 if not available + int scale = (column != null) ? column.getTypeInfo().getScale() : 7; + String dtoString = dto.toString(); + + // If scale is 0, return as-is but remove any existing fractional seconds + if (scale == 0) { + // Remove fractional seconds if present + int dotIndex = dtoString.indexOf('.'); + if (dotIndex > 0) { + int spaceIndex = dtoString.indexOf(' ', dotIndex); + if (spaceIndex > 0) { + return dtoString.substring(0, dotIndex) + dtoString.substring(spaceIndex); + } + } + return dtoString; + } + + // Find where to insert/replace fractional seconds + int dotIndex = dtoString.indexOf('.'); + int spaceIndex = dtoString.lastIndexOf(' '); + + if (dotIndex > 0 && spaceIndex > dotIndex) { + // Has fractional seconds, replace them + String prefix = dtoString.substring(0, dotIndex); + String suffix = dtoString.substring(spaceIndex); + java.sql.Timestamp ts = dto.getTimestamp(); + String fractional = fractionalSecondsString(ts.getNanos(), scale); + return prefix + fractional + suffix; + } else if (spaceIndex > 0) { + // No fractional seconds, add them + String prefix = dtoString.substring(0, spaceIndex); + String suffix = dtoString.substring(spaceIndex); + java.sql.Timestamp ts = dto.getTimestamp(); + String fractional = fractionalSecondsString(ts.getNanos(), scale); + return prefix + fractional + suffix; } else { - return val.longValue(); + // Fallback - couldn't parse format + return dtoString; } - } else if (o instanceof Number) { - return new Long(((Number) o).longValue()); - } else if (o instanceof String) { - return new Long(((String) o).trim()); - } - loggerExternal.exiting(getClassNameLogging(), "getLong", o); - // return null != o ? Long.valueOf(o.toString()) : 0; - return 0; - } - -/* - @Override - protected void skipColumns(int columnsToSkip, boolean discardValues) throws SQLServerException { - // Do nothing - //super.skipColumns(columnsToSkip, discardValues); - } -*/ + } + + // Fallback to default string conversion + return temporalValue.toString(); + } + + // Copy of DDC.fractionalSecondsString method to match exact behavior + private static String fractionalSecondsString(long subSecondNanos, int scale) { + assert 0 <= subSecondNanos && subSecondNanos < 1_000_000_000; // Nanos.PER_SECOND + assert 0 <= scale && scale <= 7; // TDS.MAX_FRACTIONAL_SECONDS_SCALE + + // Fast path for 0 scale (avoids creation of two BigDecimal objects and + // two Strings when the answer is going to be "" anyway...) + if (0 == scale) + return ""; + + return java.math.BigDecimal.valueOf(subSecondNanos % 1_000_000_000, 9).setScale(scale).toPlainString() + .substring(1); + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java index e63fab0ea3..5ebc54b3d1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java @@ -5041,7 +5041,7 @@ public void refreshRow() throws SQLException { loggerExternal.exiting(getClassNameLogging(), "refreshRow"); } - private void doRefreshRow() throws SQLServerException { + protected void doRefreshRow() throws SQLServerException { assert hasCurrentRow(); // Save off the current row offset into the fetch buffer so that we can attempt to From ea4000a9877da0088948a76835fe68190efc9120 Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Wed, 5 Nov 2025 15:32:52 +0530 Subject: [PATCH 7/7] fixed build failures --- .../sqlserver/jdbc/SQLServerPrefetchedResultSet.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java index c6e0282199..37cdab4479 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPrefetchedResultSet.java @@ -8,7 +8,13 @@ import java.util.Locale; import java.util.UUID; +/** + * A prefetched result set that caches row data for improved performance. + * This class extends SQLServerResultSet to provide cached access to row data, + * reducing the number of TDS reader operations needed for repeated column access. + */ public class SQLServerPrefetchedResultSet extends SQLServerResultSet { + /** Array to cache the current row data */ private Object[] row = null; SQLServerPrefetchedResultSet(SQLServerStatement stmtIn) throws SQLServerException { @@ -55,6 +61,7 @@ public boolean next() throws SQLServerException { * Updates the current row with the values in the row cache. * * @throws SQLException if a database access error occurs + * @throws SQLServerException if a database access error occurs */ @Override public void updateRow() throws SQLException { @@ -67,7 +74,7 @@ public void updateRow() throws SQLException { /** * Updates the current row with the values in the row cache. * - * @throws SQLException if a database access error occurs + * @throws SQLServerException if a database access error occurs */ @Override public void updateString(int columnIndex, String stringValue) throws SQLServerException { @@ -124,7 +131,7 @@ public boolean first() throws SQLException { /** * Moves the cursor to the specified row in the result set. * - * @param row the row number to move to (1-based) + * @param rows the number of rows to move (positive for forward, negative for backward) * @return true if the cursor is moved to a valid row, false if the row is invalid * @throws SQLException if a database access error occurs */