diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 6874deab4..42ae6647d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -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); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index da7688e60..480f36ba3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -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); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index 12cc843b7..8b60eccb8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -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); } @Override diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index e4b1d59ee..1ffffa6f0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -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 = { @@ -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(), @@ -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 @@ -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); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 2128f9cef..244cfb696 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -1246,16 +1246,25 @@ public final java.sql.ResultSetMetaData getMetaData() throws SQLServerException, */ 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) { + // Ignore empty result set errors, otherwise propagate the server error. + if (!ex.getMessage().equals(SQLServerException.getErrString("R_noResultset"))) { + throw ex; + } + } } } return emptyResultSet; diff --git a/src/main/java/microsoft/sql/DateTimeOffset.java b/src/main/java/microsoft/sql/DateTimeOffset.java index bf9e95c7b..dd1de85b2 100644 --- a/src/main/java/microsoft/sql/DateTimeOffset.java +++ b/src/main/java/microsoft/sql/DateTimeOffset.java @@ -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; @@ -190,7 +192,6 @@ public String toString() { .substring(2), // -> "123456" formattedOffset); } - return result; } @@ -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. * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java index 5309780ca..646ad75e9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java @@ -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; @@ -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"); + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index 29a5e21dd..06ab713c8 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -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 ''"}, - {"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."},}; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetadataGetIndexInfoTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetadataGetIndexInfoTest.java new file mode 100644 index 000000000..794eb721a --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetadataGetIndexInfoTest.java @@ -0,0 +1,177 @@ +package com.microsoft.sqlserver.jdbc.databasemetadata; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; + +public class DatabaseMetadataGetIndexInfoTest extends AbstractTest { + + private static String tableName = AbstractSQLGenerator.escapeIdentifier("DBMetadataTestTable"); + private static String col1Name = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("col1")); + private static String col2Name = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("col2")); + private static String col3Name = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("col3")); + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + @BeforeEach + public void init() throws SQLException { + try (Connection con = getConnection()) { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + TestUtils.dropTableIfExists(tableName, stmt); + String createTableSQL = "CREATE TABLE " + tableName + " (" + col1Name + " INT, " + col2Name + " INT, " + + col3Name + " INT)"; + + stmt.executeUpdate(createTableSQL); + assertNull(connection.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateTableConnection")); + assertNull(stmt.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateTableStatement")); + + String createClusteredIndexSQL = "CREATE CLUSTERED INDEX IDX_Clustered ON " + tableName + "(" + col1Name + + ")"; + stmt.executeUpdate(createClusteredIndexSQL); + assertNull(connection.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateIndexConnection")); + assertNull(stmt.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateIndexStatement")); + + String createNonClusteredIndexSQL = "CREATE NONCLUSTERED INDEX IDX_NonClustered ON " + tableName + "(" + + col2Name + ")"; + stmt.executeUpdate(createNonClusteredIndexSQL); + assertNull(connection.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateIndexConnection")); + assertNull(stmt.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateIndexStatement")); + + String createColumnstoreIndexSQL = "CREATE COLUMNSTORE INDEX IDX_Columnstore ON " + tableName + "(" + + col3Name + ")"; + stmt.executeUpdate(createColumnstoreIndexSQL); + assertNull(connection.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateIndexConnection")); + assertNull(stmt.getWarnings(), TestResource.getResource("R_noSQLWarningsCreateIndexStatement")); + } + con.commit(); + } + } + + @AfterEach + public void terminate() throws SQLException { + try (Connection con = getConnection(); Statement stmt = con.createStatement()) { + try { + TestUtils.dropTableIfExists(tableName, stmt); + } catch (SQLException e) { + fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); + } + } + } + + @Test + public void testGetIndexInfo() throws SQLException { + ResultSet rs1, rs2 = null; + try (Connection connection = getConnection(); Statement stmt = connection.createStatement()) { + String catalog = connection.getCatalog(); + String schema = "dbo"; + String table = "DBMetadataTestTable"; + DatabaseMetaData dbMetadata = connection.getMetaData(); + rs1 = dbMetadata.getIndexInfo(catalog, schema, table, false, false); + + boolean hasClusteredIndex = false; + boolean hasNonClusteredIndex = false; + boolean hasColumnstoreIndex = false; + + String query = "SELECT " + " db_name() AS CatalogName, " + " sch.name AS SchemaName, " + + " t.name AS TableName, " + " i.name AS IndexName, " + " i.type_desc AS IndexType, " + + " i.is_unique AS IsUnique, " + " c.name AS ColumnName, " + + " ic.key_ordinal AS ColumnOrder " + "FROM " + " sys.indexes i " + "INNER JOIN " + + " sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id " + + "INNER JOIN " + " sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id " + + "INNER JOIN " + " sys.tables t ON i.object_id = t.object_id " + "INNER JOIN " + + " sys.schemas sch ON t.schema_id = sch.schema_id " + + + "WHERE t.name = '" + table + "' " + "AND sch.name = '" + schema + "' " + "ORDER BY " + + " t.name, i.name, ic.key_ordinal;"; + rs2 = stmt.executeQuery(query); + + while (rs1.next() && rs2.next()) { + String indexType = rs1.getString("IndexType"); + String indexName = rs1.getString("IndexName"); + String catalogName = rs1.getString("CatalogName"); + String schemaName = rs1.getString("SchemaName"); + String tableName = rs1.getString("TableName"); + boolean isUnique = rs1.getBoolean("IsUnique"); + String columnName = rs1.getString("ColumnName"); + int columnOrder = rs1.getInt("ColumnOrder"); + + assertEquals(catalogName, rs2.getString("CatalogName")); + assertEquals(schemaName, rs2.getString("SchemaName")); + assertEquals(tableName, rs2.getString("TableName")); + assertEquals(indexName, rs2.getString("IndexName")); + assertEquals(indexType, rs2.getString("IndexType")); + assertEquals(isUnique, rs2.getBoolean("IsUnique")); + assertEquals(columnName, rs2.getString("ColumnName")); + assertEquals(columnOrder, rs2.getInt("ColumnOrder")); + + if (indexType.contains("COLUMNSTORE")) { + hasColumnstoreIndex = true; + } else if (indexType.equals("CLUSTERED")) { + hasClusteredIndex = true; + } else if (indexType.equals("NONCLUSTERED")) { + hasNonClusteredIndex = true; + } + } + + assertTrue(hasColumnstoreIndex, "COLUMNSTORE index not found."); + assertTrue(hasClusteredIndex, "CLUSTERED index not found."); + assertTrue(hasNonClusteredIndex, "NONCLUSTERED index not found."); + } + } + + @Test + public void testGetIndexInfoCaseSensitivity() throws SQLException { + ResultSet rs1, rs2 = null; + try (Connection connection = getConnection()) { + String catalog = connection.getCatalog(); + String schema = "dbo"; + String table = "DBMetadataTestTable"; + + DatabaseMetaData dbMetadata = connection.getMetaData(); + rs1 = dbMetadata.getIndexInfo(catalog, schema, table, false, false); + rs2 = dbMetadata.getIndexInfo(catalog, schema, table.toUpperCase(), false, false); + + while (rs1.next() && rs2.next()) { + String indexType = rs1.getString("IndexType"); + String indexName = rs1.getString("IndexName"); + String catalogName = rs1.getString("CatalogName"); + String schemaName = rs1.getString("SchemaName"); + String tableName = rs1.getString("TableName"); + boolean isUnique = rs1.getBoolean("IsUnique"); + String columnName = rs1.getString("ColumnName"); + int columnOrder = rs1.getInt("ColumnOrder"); + + assertEquals(catalogName, rs2.getString("CatalogName")); + assertEquals(schemaName, rs2.getString("SchemaName")); + assertEquals(tableName, rs2.getString("TableName")); + assertEquals(indexName, rs2.getString("IndexName")); + assertEquals(indexType, rs2.getString("IndexType")); + assertEquals(isUnique, rs2.getBoolean("IsUnique")); + assertEquals(columnName, rs2.getString("ColumnName")); + assertEquals(columnOrder, rs2.getInt("ColumnOrder")); + } + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java index 445d23dd0..d1bb30a2c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java @@ -1945,6 +1945,34 @@ public void testDateTimeOffsetValueOfOffsetDateTime() throws Exception { assertEquals(expected, DateTimeOffset.valueOf(roundUp).getOffsetDateTime()); assertEquals(expected, DateTimeOffset.valueOf(roundDown).getOffsetDateTime()); } + + @Test + public void testPreGregorianDateTime() throws Exception { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);) { + + conn.setAutoCommit(false); + TestUtils.dropTableIfExists(escapedTableName, stmt); + + stmt.executeUpdate("CREATE TABLE " + escapedTableName + " (dob datetimeoffset(7) null)"); + stmt.executeUpdate("INSERT INTO " + escapedTableName + " VALUES ('1500-12-16 00:00:00.0000000+08:00')"); + stmt.executeUpdate("INSERT INTO " + escapedTableName + " VALUES ('1400-09-27 09:30:00.0000000+08:00')"); + stmt.executeUpdate("INSERT INTO " + escapedTableName + " VALUES ('2024-12-16 23:40:00.0000000+08:00')"); + + try (ResultSet rs = stmt.executeQuery("select dob from " + escapedTableName + " order by dob")) { + while (rs.next()) { + String strDateTimeOffset = rs.getString(1).substring(0, 10); + DateTimeOffset objDateTimeOffset = (DateTimeOffset) rs.getObject(1); + OffsetDateTime objOffsetDateTime = objDateTimeOffset.getOffsetDateTime(); + + String strOffsetDateTime = objOffsetDateTime.toString().substring(0, 10); + assertEquals(strDateTimeOffset, strOffsetDateTime, "Mismatch found in DateTimeOffset : " + + objDateTimeOffset + " and OffsetDateTime : " + objOffsetDateTime); + } + } + TestUtils.dropTableIfExists(escapedTableName, stmt); + } + } static LocalDateTime getUnstorableValue() throws Exception { ZoneId systemTimezone = ZoneId.systemDefault(); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java index 23c89071d..87e0994aa 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -13,8 +13,10 @@ import java.lang.reflect.Field; import java.sql.BatchUpdateException; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; @@ -124,6 +126,25 @@ public void testPreparedStatementWithSpPrepare() throws SQLException { } } } + + @Test + void testDatabaseQueryMetaData() throws SQLException { + try (Connection connection = getConnection()) { + try (SQLServerPreparedStatement stmt = (SQLServerPreparedStatement) connection.prepareStatement( + "select 1 as \"any questions ???\"")) { + ResultSetMetaData metaData = stmt.getMetaData(); + String actualLabel = metaData.getColumnLabel(1); + String actualName = metaData.getColumnName(1); + + String expected = "any questions ???"; + assertEquals(expected, actualLabel, "Column label should match the expected value"); + assertEquals(expected, actualName, "Column name should match the expected value"); + } + } catch (SQLException e) { + e.printStackTrace(); + fail("SQLException occurred during test: " + e.getMessage()); + } + } @Test public void testPreparedStatementParamNameSpacingWithMultipleParams() throws SQLException { @@ -927,5 +948,5 @@ private static void dropTables() throws Exception { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName5), stmt); } } - + }