Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated getIndexInfo() to include Columnstore indexes by using custom query. #2566

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2446,7 +2446,7 @@ Connection connectInternal(Properties propsIn,
if (null != sPropValue)
validateMaxSQLLoginName(sPropKey, sPropValue);
else
activeConnectionProperties.setProperty(sPropKey, SQLServerDriver.DEFAULT_APP_NAME);
activeConnectionProperties.setProperty(sPropKey, SQLServerDriver.constructedAppName);

sPropKey = SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString();
sPropValue = activeConnectionProperties.getProperty(sPropKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public void setApplicationName(String applicationName) {
@Override
public String getApplicationName() {
return getStringProperty(connectionProps, SQLServerDriverStringProperty.APPLICATION_NAME.toString(),
SQLServerDriverStringProperty.APPLICATION_NAME.getDefaultValue());
SQLServerDriver.constructedAppName);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,39 +1197,47 @@ private ResultSet executeSPFkeys(String[] procParams) throws SQLException {
}
}

private static final String[] getIndexInfoColumnNames = { /* 1 */ TABLE_CAT, /* 2 */ TABLE_SCHEM,
/* 3 */ TABLE_NAME, /* 4 */ NON_UNIQUE, /* 5 */ INDEX_QUALIFIER, /* 6 */ INDEX_NAME, /* 7 */ TYPE,
/* 8 */ ORDINAL_POSITION, /* 9 */ COLUMN_NAME, /* 10 */ ASC_OR_DESC, /* 11 */ CARDINALITY, /* 12 */ PAGES,
/* 13 */ FILTER_CONDITION};

@Override
public java.sql.ResultSet getIndexInfo(String cat, String schema, String table, boolean unique,
boolean approximate) throws SQLServerException, SQLTimeoutException {
boolean approximate) throws SQLException {
if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString());
}
checkClosed();
/*
* sp_statistics [ @table_name = ] 'table_name' [ , [ @table_owner = ] 'owner' ] [ , [ @table_qualifier = ]
* 'qualifier' ] [ , [ @index_name = ] 'index_name' ] [ , [ @is_unique = ] 'is_unique' ] [ , [ @accuracy = ]
* 'accuracy' ]
*/
String[] arguments = new String[6];
arguments[0] = table;
arguments[1] = schema;
arguments[2] = cat;
// use default for index name
arguments[3] = "%"; // index name % is default
if (unique)
arguments[4] = "Y"; // is_unique
else
arguments[4] = "N";
if (approximate)
arguments[5] = "Q";
else
arguments[5] = "E";
return getResultSetWithProvidedColumnNames(cat, CallableHandles.SP_STATISTICS, arguments,
getIndexInfoColumnNames);
* Replaced the use of the sp_statistics stored procedure with a custom query to retrieve index information.
*
* Reason for change:
* The sp_statistics procedure was not returning Columnstore indexes, which was limiting the results.
* To address this issue and include all index types (Clustered, NonClustered, and Columnstore), a direct
* SQL query using sys.indexes, sys.index_columns, and related system views was implemented.
*
* This query ensures a complete set of index information, regardless of the index type, as a workaround for
* the limitations of sp_statistics.
*
* GitHub Issue: #2546 - Columnstore indexes were missing from sp_statistics results.
*/
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append("SELECT ")
.append("db_name() AS CatalogName, ")
.append("sch.name AS SchemaName, ")
.append("t.name AS TableName, ")
.append("i.name AS IndexName, ")
.append("i.type_desc AS IndexType, ")
.append("i.is_unique AS IsUnique, ")
.append("c.name AS ColumnName, ")
.append("ic.key_ordinal AS ColumnOrder ")
.append("FROM sys.indexes i ")
.append("INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id ")
.append("INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id ")
.append("INNER JOIN sys.tables t ON i.object_id = t.object_id ")
.append("INNER JOIN sys.schemas sch ON t.schema_id = sch.schema_id ")
.append("WHERE t.name = '").append(table).append("' ")
.append("AND sch.name = '").append(schema).append("' ")
.append("ORDER BY t.name, i.name, ic.key_ordinal");

String query = queryBuilder.toString();
return getResultSetFromInternalQueries(cat, query);
Comment on lines +1220 to +1240
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. Perf: Unnecessary append operations should be changed to a single, static, final string.
  2. table and schema are external inputs. Don't append them into the SQL (that's a SQL injection vulnerability). Use a prepared statement with defined parameters.

}

@Override
Expand Down
33 changes: 32 additions & 1 deletion src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,32 @@ public final class SQLServerDriver implements java.sql.Driver {
static final String AUTH_DLL_NAME = "mssql-jdbc_auth-" + SQLJdbcVersion.MAJOR + "." + SQLJdbcVersion.MINOR + "."
+ SQLJdbcVersion.PATCH + "." + Util.getJVMArchOnWindows() + SQLJdbcVersion.RELEASE_EXT;
static final String DEFAULT_APP_NAME = "Microsoft JDBC Driver for SQL Server";
static final String APP_NAME_TEMPLATE = "Microsoft JDBC - %s, %s - %s";
static final String constructedAppName;
static {
constructedAppName = getAppName();
}

/**
* Constructs the application name using system properties for OS, platform, and architecture.
* If any of the properties cannot be fetched, it falls back to the default application name.
* Format -> Microsoft JDBC - {OS}, {Platform} - {architecture}
*
* @return the constructed application name or the default application name if properties are not available
*/
static String getAppName() {
String osName = System.getProperty("os.name", "");
String osArch = System.getProperty("os.arch", "");
String javaVmName = System.getProperty("java.vm.name", "");
String javaVmVersion = System.getProperty("java.vm.version", "");
String platform = javaVmName.isEmpty() || javaVmVersion.isEmpty() ? "" : javaVmName + " " + javaVmVersion;

if (osName.isEmpty() && platform.isEmpty() && osArch.isEmpty()) {
return DEFAULT_APP_NAME;
}
return String.format(APP_NAME_TEMPLATE, osName, platform, osArch);
}

private static final String[] TRUE_FALSE = {"true", "false"};

private static final SQLServerDriverPropertyInfo[] DRIVER_PROPERTIES = {
Expand All @@ -741,7 +766,7 @@ public final class SQLServerDriver implements java.sql.Driver {
SQLServerDriverStringProperty.APPLICATION_INTENT.getDefaultValue(), false,
new String[] {ApplicationIntent.READ_ONLY.toString(), ApplicationIntent.READ_WRITE.toString()}),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.APPLICATION_NAME.toString(),
SQLServerDriverStringProperty.APPLICATION_NAME.getDefaultValue(), false, null),
SQLServerDriverStringProperty.APPLICATION_NAME.getDefaultValue(), false, null),
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString(),
SQLServerDriverStringProperty.COLUMN_ENCRYPTION.getDefaultValue(), false,
new String[] {ColumnEncryptionSetting.DISABLED.toString(),
Expand Down Expand Up @@ -1028,6 +1053,9 @@ String getClassNameLogging() {
drLogger.finer("Error registering driver: " + e);
}
}
if (loggerExternal.isLoggable(Level.FINE)) {
loggerExternal.log(Level.FINE, "Application Name: " + SQLServerDriver.constructedAppName);
}
}

// Check for jdk.net.ExtendedSocketOptions to set TCP keep-alive options for idle connection resiliency
Expand Down Expand Up @@ -1266,6 +1294,9 @@ public java.sql.Connection connect(String url, Properties suppliedProperties) th
Properties connectProperties = parseAndMergeProperties(url, suppliedProperties);
if (connectProperties != null) {
result = DriverJDBCVersion.getSQLServerConnection(toString());
if (connectProperties.getProperty(SQLServerDriverStringProperty.APPLICATION_NAME.toString()) == null) {
connectProperties.setProperty(SQLServerDriverStringProperty.APPLICATION_NAME.toString(), SQLServerDriver.constructedAppName);
}
result.connect(connectProperties, null);
}
loggerExternal.exiting(getClassNameLogging(), "connect", result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1246,16 +1246,25 @@
*/
private SQLServerResultSet buildExecuteMetaData() throws SQLServerException, SQLTimeoutException {
String fmtSQL = userSQL;

SQLServerResultSet emptyResultSet = null;
try {
fmtSQL = replaceMarkerWithNull(fmtSQL);
internalStmt = (SQLServerStatement) connection.createStatement();
emptyResultSet = internalStmt.executeQueryInternal("set fmtonly on " + fmtSQL + "\nset fmtonly off");
} catch (SQLServerException sqle) {
// Ignore empty result set errors, otherwise propagate the server error.
if (!sqle.getMessage().equals(SQLServerException.getErrString("R_noResultset"))) {
throw sqle;
//try by replacing ? characters in case that was an issue
try {
fmtSQL = replaceMarkerWithNull(fmtSQL);
internalStmt = (SQLServerStatement) connection.createStatement();
emptyResultSet = internalStmt.executeQueryInternal("set fmtonly on " + fmtSQL + "\nset fmtonly off");
} catch (SQLServerException ex) {

Check warning on line 1262 in src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1262 was not covered by tests
// Ignore empty result set errors, otherwise propagate the server error.
if (!ex.getMessage().equals(SQLServerException.getErrString("R_noResultset"))) {
throw ex;

Check warning on line 1265 in src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1265 was not covered by tests
}
}
}
}
return emptyResultSet;
Expand Down
33 changes: 27 additions & 6 deletions src/main/java/microsoft/sql/DateTimeOffset.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package microsoft.sql;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
Expand Down Expand Up @@ -190,7 +192,6 @@ public String toString() {
.substring(2), // -> "123456"
formattedOffset);
}

return result;
}

Expand Down Expand Up @@ -257,12 +258,32 @@ public java.sql.Timestamp getTimestamp() {
* @return OffsetDateTime equivalent to this DateTimeOffset object.
*/
public java.time.OffsetDateTime getOffsetDateTime() {
java.time.ZoneOffset zoneOffset = java.time.ZoneOffset.ofTotalSeconds(60 * minutesOffset);
java.time.LocalDateTime localDateTime = java.time.LocalDateTime.ofEpochSecond(utcMillis / 1000, nanos,
zoneOffset);
return java.time.OffsetDateTime.of(localDateTime, zoneOffset);
// Format the offset as +hh:mm or -hh:mm. Zero offset is formatted as +00:00.
String formattedOffset = (minutesOffset < 0) ?
String.format(Locale.US, "-%1$02d:%2$02d", -minutesOffset / 60, -minutesOffset % 60) :
String.format(Locale.US, "+%1$02d:%2$02d", minutesOffset / 60, minutesOffset % 60);

// Create a Calendar instance with the time zone set to GMT plus the formatted offset
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT" + formattedOffset), Locale.US);
// Initialize the calendar with the UTC milliseconds value
calendar.setTimeInMillis(utcMillis);

// Extract the date and time components from the calendar
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // Calendar.MONTH is zero-based
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);

// Create the ZoneOffset from the minutesOffset
ZoneOffset offset = ZoneOffset.ofTotalSeconds(minutesOffset * 60);

// Create and return the OffsetDateTime
return OffsetDateTime.of(year, month, day, hour, minute, second, nanos, offset);
}



/**
* Returns this DateTimeOffset object's offset value.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.junit.Assert.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.sql.Connection;
Expand Down Expand Up @@ -190,4 +192,78 @@ public void testConnectionDriver() throws SQLException {
}
}
}

/**
* test application name
*
* @throws SQLException
*/
@Test
public void testApplicationName() throws SQLException {
try (Connection conn = DriverManager.getConnection(connectionString);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT program_name FROM sys.dm_exec_sessions WHERE session_id = @@SPID")) {
if (rs.next()) {
assertEquals(SQLServerDriver.constructedAppName, rs.getString("program_name"));
}
} catch (SQLException e) {
fail(e.getMessage());
}
}

/**
* test application name by executing select app_name()
*
* @throws SQLException
*/
@Test
public void testApplicationNameUsingApp_Name() throws SQLException {
try (Connection conn = DriverManager.getConnection(connectionString);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT app_name()")) {
if (rs.next()) {
assertEquals(SQLServerDriver.constructedAppName, rs.getString(1));
}
} catch (SQLException e) {
fail(e.getMessage());
}
}

/**
* test application name by executing select app_name()
*
* @throws SQLException
*/
@Test
public void testAppNameWithSpecifiedApplicationName() throws SQLException {
String url = connectionString + ";applicationName={0123456789012345678901234567890123456789012345678901234567890123456789012345678901234589012345678901234567890123456789012345678}";

try (Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT app_name()")) {
if (rs.next()) {
assertEquals("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234589012345678901234567890123456789012345678", rs.getString(1));
}
} catch (SQLException e) {
fail(e.getMessage());
}
}

/**
* test application name when system properties are empty
*
*/
@Test
public void testGetAppName() {
String appName = SQLServerDriver.getAppName();
assertNotNull(appName, "Application name should not be null");
assertFalse(appName.isEmpty(), "Application name should not be empty");

System.setProperty("os.name", "");
System.setProperty("os.arch", "");
System.setProperty("java.vm.name", "");
System.setProperty("java.vm.version", "");
String defaultAppName = SQLServerDriver.getAppName();
assertEquals(SQLServerDriver.DEFAULT_APP_NAME, defaultAppName, "Application name should be the default one");
}
}
6 changes: 5 additions & 1 deletion src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,5 +216,9 @@ protected Object[][] getContents() {
{"R_expectedClassDoesNotMatchActualClass",
"Expected column class {0} does not match actual column class {1} for column {2}."},
{"R_loginFailedMI", "Login failed for user '<token-identified principal>'"},
{"R_MInotAvailable", "Managed Identity authentication is not available"},};
{"R_MInotAvailable", "Managed Identity authentication is not available"},
{"R_noSQLWarningsCreateTableConnection", "Expecting NO SQLWarnings from 'create table', at Connection."},
{"R_noSQLWarningsCreateTableStatement", "Expecting NO SQLWarnings from 'create table', at Statement."},
{"R_noSQLWarningsCreateIndexConnection", "Expecting NO SQLWarnings from 'create index', at Connection."},
{"R_noSQLWarningsCreateIndexStatement", "Expecting NO SQLWarnings from 'create index', at Statement."},};
}
Loading
Loading