diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 4739ccc9b..a5f299320 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/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/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); } } - + }