Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f8811ea
Implement Sybase migration support with dynamicPrepare property
divang Oct 24, 2025
7dac544
Revert "Implement Sybase migration support with dynamicPrepare property"
divang Oct 24, 2025
cfb0e43
Add SqlServerPreparedStatementExpander with comprehensive test coverage
divang Nov 11, 2025
73cc7e2
Fix SqlServerPreparedStatementExpander test failures
divang Nov 11, 2025
b5f2299
Add 7 new E2E test cases for various SQL Server data types
divang Nov 11, 2025
9acdfc8
Added Sybase test code
divang Nov 11, 2025
c49e2d3
Merge branch 'main' of https://github.com/microsoft/mssql-jdbc into d…
divang Nov 17, 2025
47009b3
Add comprehensive test coverage for EXEC prepare method (#2834)
Ananya2 Nov 17, 2025
70d8b30
Fixed PrepareMethod - EXEC method related test cases
divang Nov 17, 2025
6f5de5c
Fixed pom.xml typo
divang Nov 17, 2025
0615022
Remove Sybase-related test files
divang Nov 27, 2025
676f27d
Consolidate SQL parameter expansion into SQLServerConnection
divang Nov 27, 2025
665951c
Fix parameter expansion to respect sendStringParametersAsUnicode and …
divang Nov 27, 2025
caf775c
Apply sendStringParametersAsUnicode config to Clob and fallback strin…
divang Nov 27, 2025
f1d26c1
Refactor replaceParameterMarkers to simplify parameter substitution
divang Nov 28, 2025
ba3b584
Fixed batch update count issue and cleaned the code
divang Dec 1, 2025
37ebc5c
When prepareMethod is set to 'exec', batch execution was failing enti…
divang Dec 2, 2025
c7712a9
Update count in case of batch related sql statements
divang Dec 2, 2025
ed847f5
Batching of SQL statement in case of exec preparedmethod
divang Dec 4, 2025
d01859f
Added replaceParameterMarkersWithValues() method and propogated execp…
divang Dec 8, 2025
8e2db67
Correct temp table naming and constraint violation assertions in Batc…
divang Dec 15, 2025
9adf6bf
Cleaned up unused code.
divang Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
Expand All @@ -29,6 +31,7 @@
import java.sql.Savepoint;
import java.sql.Statement;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
Expand Down Expand Up @@ -230,6 +233,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
/** Engine Edition 11 = Azure Synapse serverless SQL pool */
private static final int ENGINE_EDITION_SQL_AZURE_SYNAPSE_SERVERLESS_SQL_POOL = 11;

// --- SQL Parameter Expansion Methods ---
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we confirm if client-side formatting is really needed or can SQL Server's cast can handle formats automatically?

private static final SimpleDateFormat TIME_FMT = new SimpleDateFormat("HH:mm:ss");
private static final SimpleDateFormat TS_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

/**
* Azure SQL server endpoints
*/
Expand Down Expand Up @@ -8453,6 +8461,163 @@ String replaceParameterMarkers(String sqlSrc, int[] paramPositions, Parameter[]
return new String(sqlDst, 0, dstBegin);
}

String replaceParameterMarkersWithValues(String sqlSrc, int[] paramPositions, Parameter[] params,
boolean isReturnValueSyntax) throws SQLServerException {

// EXEC method: substitute actual parameter values directly into SQL
if (params == null || params.length == 0) {
return sqlSrc;
}

StringBuilder result = new StringBuilder(sqlSrc.length() + params.length * 20);
try {
// Estimate capacity for the result
int srcBegin = 0;

for (int paramIndex = 0; paramIndex < paramPositions.length; paramIndex++) {
int srcEnd = paramPositions[paramIndex];

// Append SQL text before this parameter marker
result.append(sqlSrc, srcBegin, srcEnd);

// Get parameter value and format it
Object value = null;
if (params[paramIndex] != null) {
value = params[paramIndex].getSetterValue();
}

// Append formatted literal value
result.append(formatLiteralValue(value));

// Move past the '?' marker
srcBegin = srcEnd + 1;
}

// Append remaining SQL after last parameter
result.append(sqlSrc, srcBegin, sqlSrc.length());
} catch (Exception e) {
throw new SQLServerException("Error during parameter replacement", e);
}

return result.toString();
}

// Format Java value into a T-SQL literal safe for SQL Server
private String formatLiteralValue(Object value) throws SQLException {
if (value == null)
return "NULL";

else if (value instanceof String) {
String prefix = sendStringParametersAsUnicode() ? "N'" : "'";
return prefix + escapeSQLString((String) value) + "'";
}

else if (value instanceof Character) {
String prefix = sendStringParametersAsUnicode() ? "N'" : "'";
return prefix + escapeSQLString(value.toString()) + "'";
}

else if (value instanceof Boolean) {
return ((Boolean) value) ? "1" : "0";
}

else if (value instanceof java.math.BigDecimal) {
// Use toPlainString() to avoid scientific notation
java.math.BigDecimal bd = (java.math.BigDecimal) value;
String plainStr = bd.toPlainString();

// For very large or high-precision decimals, wrap in CAST to preserve precision
// SQL Server decimal max precision is 38, scale max 38
int precision = bd.precision();
int scale = bd.scale();

if (precision > 18 || scale > 6) {
// Need explicit CAST for high precision numbers
// Clamp to SQL Server limits: decimal(38, min(scale, 38))
// Ensure scale <= precision
int sqlPrecision = Math.min(precision, 38);
int sqlScale = Math.min(Math.min(scale, 38), sqlPrecision);
return "CAST(" + plainStr + " AS DECIMAL(" + sqlPrecision + "," + sqlScale + "))";
}
return plainStr;
}

// Integer types can use toString() safely
else if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long) {
return value.toString();
}

// Float and Double need special handling to avoid precision loss and scientific
// notation
else if (value instanceof Float) {
// Convert Float to BigDecimal for precise representation
return new java.math.BigDecimal(value.toString()).toPlainString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test cases for MIN and MAX values of decimal, BigDecimal, Float, Double, Integer, BigInteger.

}

else if (value instanceof Double) {
// Convert Double to BigDecimal for precise representation
return new java.math.BigDecimal(value.toString()).toPlainString();
}

else if (value instanceof java.sql.Date) {
return "CAST('" + DATE_FMT.format((java.util.Date) value) + "' AS DATE)";
}

else if (value instanceof java.sql.Time) {
return "CAST('" + TIME_FMT.format((java.util.Date) value) + "' AS TIME)";
}

else if (value instanceof java.sql.Timestamp) {
return "CAST('" + TS_FMT.format((java.util.Date) value) + "' AS DATETIME2)";
}

else if (value instanceof java.util.Date) {
// generic java.util.Date -> timestamp
return "CAST('" + TS_FMT.format((java.util.Date) value) + "' AS DATETIME2)";
}

else if (value instanceof byte[]) {
return bytesToHexLiteral((byte[]) value);
}

else if (value instanceof Blob) {
Blob b = (Blob) value;
int len = (int) b.length();
byte[] bytes = b.getBytes(1, len);
return bytesToHexLiteral(bytes);
}

else if (value instanceof Clob) {
Clob c = (Clob) value;
String s = c.getSubString(1, (int) c.length());
String prefix = sendStringParametersAsUnicode() ? "N'" : "'";
return prefix + escapeSQLString(s) + "'";
}

else {
// fallback
String prefix = sendStringParametersAsUnicode() ? "N'" : "'";
return prefix + escapeSQLString(value.toString()) + "'";
}
}

private String escapeSQLString(String s) {
if (s == null || s.isEmpty())
return "";
// double single quotes
return s.replace("'", "''");
}

private String bytesToHexLiteral(byte[] bytes) {
if (bytes == null || bytes.length == 0)
return "0x";
StringBuilder sb = new StringBuilder(bytes.length * 2 + 2);
sb.append("0x");
for (byte b : bytes)
sb.append(String.format("%02X", b));
return sb.toString();
}

/**
* Makes a SQL Server style parameter name.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,8 @@ public String toString() {

enum PrepareMethod {
PREPEXEC("prepexec"), // sp_prepexec, default prepare method
PREPARE("prepare");
PREPARE("prepare"),
EXEC("exec");

private final String value;

Expand Down Expand Up @@ -840,7 +841,7 @@ public final class SQLServerDriver implements java.sql.Driver {
SQLServerDriverStringProperty.SERVER_CERTIFICATE.getDefaultValue(), false, null),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.PREPARE_METHOD.toString(),
SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue(), false,
new String[] {PrepareMethod.PREPEXEC.toString(), PrepareMethod.PREPARE.toString()}),
new String[] {PrepareMethod.PREPEXEC.toString(), PrepareMethod.PREPARE.toString(), PrepareMethod.EXEC.toString()}),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.FAILOVER_PARTNER.toString(),
SQLServerDriverStringProperty.FAILOVER_PARTNER.getDefaultValue(), false, null),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString(),
Expand Down
Loading
Loading