From 59750f751101ea02f480afc7d22db2c5f88a7076 Mon Sep 17 00:00:00 2001 From: Petro Tiurin <93913847+ptiurin@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:28:49 +0100 Subject: [PATCH] feat(FIR-34158): populating SQLState in firebolt errors (#432) --- .../firebolt/jdbc/client/FireboltClient.java | 10 +- .../jdbc/exception/FireboltException.java | 22 +++ .../com/firebolt/jdbc/exception/SQLState.java | 131 ++++++++++++++++++ .../FireboltAuthenticationService.java | 7 +- .../FireboltAuthenticationClientTest.java | 19 ++- .../gateway/FireboltGatewayUrlClientTest.java | 9 +- .../FireboltAuthenticationServiceTest.java | 18 +++ 7 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/firebolt/jdbc/exception/SQLState.java diff --git a/src/main/java/com/firebolt/jdbc/client/FireboltClient.java b/src/main/java/com/firebolt/jdbc/client/FireboltClient.java index f7b597846..042d32afc 100644 --- a/src/main/java/com/firebolt/jdbc/client/FireboltClient.java +++ b/src/main/java/com/firebolt/jdbc/client/FireboltClient.java @@ -3,6 +3,7 @@ import com.firebolt.jdbc.connection.CacheListener; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.exception.SQLState; import com.firebolt.jdbc.exception.ServerError; import com.firebolt.jdbc.exception.ServerError.Error.Location; import com.firebolt.jdbc.resultset.compress.LZ4InputStream; @@ -41,6 +42,7 @@ import java.util.stream.Collectors; import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import static java.util.Optional.ofNullable; @@ -147,7 +149,8 @@ protected void validateResponse(String host, Response response, Boolean isCompre int statusCode = response.code(); if (!isCallSuccessful(statusCode)) { if (statusCode == HTTP_UNAVAILABLE) { - throw new FireboltException(format("Could not query Firebolt at %s. The engine is not running.", host), statusCode); + throw new FireboltException(format("Could not query Firebolt at %s. The engine is not running.", host), + statusCode, SQLState.CONNECTION_FAILURE); } String errorMessageFromServer = extractErrorMessage(response, isCompress); ServerError serverError = parseServerError(errorMessageFromServer); @@ -156,11 +159,12 @@ protected void validateResponse(String host, Response response, Boolean isCompre String errorResponseMessage = format( "Server failed to execute query with the following error:%n%s%ninternal error:%n%s", processedErrorMessage, getInternalErrorWithHeadersText(response)); - if (statusCode == HTTP_UNAUTHORIZED) { + if (statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_FORBIDDEN) { getConnection().removeExpiredTokens(); throw new FireboltException(format( "Could not query Firebolt at %s. The operation is not authorized or the token is expired and has been cleared from the cache.%n%s", - host, errorResponseMessage), statusCode, processedErrorMessage); + host, errorResponseMessage), statusCode, processedErrorMessage, + SQLState.INVALID_AUTHORIZATION_SPECIFICATION); } throw new FireboltException(errorResponseMessage, statusCode, processedErrorMessage); } diff --git a/src/main/java/com/firebolt/jdbc/exception/FireboltException.java b/src/main/java/com/firebolt/jdbc/exception/FireboltException.java index 4c0ead1de..ede3445a9 100644 --- a/src/main/java/com/firebolt/jdbc/exception/FireboltException.java +++ b/src/main/java/com/firebolt/jdbc/exception/FireboltException.java @@ -39,10 +39,20 @@ public FireboltException(String message, Integer httpStatusCode, String errorMes this.errorMessageFromServer = errorMessageFromServer; } + public FireboltException(String message, Integer httpStatusCode, String errorMessageFromServer, SQLState state) { + super(message, state.getCode()); + type = getExceptionType(httpStatusCode); + this.errorMessageFromServer = errorMessageFromServer; + } + public FireboltException(String message, Throwable cause) { this(message, cause, ExceptionType.ERROR); } + public FireboltException(String message, Throwable cause, SQLState state) { + this(message, cause, ExceptionType.ERROR, state); + } + public FireboltException(String message, ExceptionType type) { super(message); this.type = type; @@ -59,6 +69,18 @@ public FireboltException(String message, Throwable cause, ExceptionType type) { errorMessageFromServer = null; } + public FireboltException(String message, Throwable cause, ExceptionType type, SQLState state) { + super(message, state.getCode(), cause); + this.type = type; + errorMessageFromServer = null; + } + + public FireboltException(String message, int httpStatusCode, SQLState state) { + super(message, state.getCode()); + type = getExceptionType(httpStatusCode); + errorMessageFromServer = null; + } + private static ExceptionType getExceptionType(Integer httpStatusCode) { if (httpStatusCode == null) { return ERROR; diff --git a/src/main/java/com/firebolt/jdbc/exception/SQLState.java b/src/main/java/com/firebolt/jdbc/exception/SQLState.java new file mode 100644 index 000000000..620118566 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/exception/SQLState.java @@ -0,0 +1,131 @@ +package com.firebolt.jdbc.exception; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +// https://en.wikipedia.org/wiki/SQLSTATE +public enum SQLState { + SUCCESS("00000"), + WARNING("01000"), + NO_DATA("02000"), + STATEMENT_STRING_DATA_RIGHT_TRUNCATION("01004"), + NULL_VALUE_NO_INDICATOR_PARAMETER("22002"), + CONNECTION_EXCEPTION("08001"), + CONNECTION_DOES_NOT_EXIST("08003"), + CONNECTION_FAILURE("08006"), + TRANSACTION_RESOLUTION_UNKNOWN("08007"), + SQL_SYNTAX_ERROR("42000"), + SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION("42601"), + DUPLICATE_KEY_VALUE("23505"), + DATA_EXCEPTION("22000"), + CHARACTER_NOT_IN_REPERTOIRE("22021"), + STRING_DATA_RIGHT_TRUNCATION("22001"), + NUMERIC_VALUE_OUT_OF_RANGE("22003"), + INVALID_DATETIME_FORMAT("22007"), + INVALID_TIME_ZONE_DISPLACEMENT_VALUE("22009"), + INVALID_ESCAPE_CHARACTER("22019"), + INVALID_PARAMETER_VALUE("22023"), + INVALID_CURSOR_STATE("24000"), + INVALID_TRANSACTION_STATE("25000"), + INVALID_AUTHORIZATION_SPECIFICATION("28000"), + INVALID_SQL_STATEMENT_NAME("26000"), + INVALID_CURSOR_NAME("34000"), + INVALID_SCHEMA_NAME("3F000"), + TRANSACTION_ROLLBACK("40000"), + SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION_IN_DIRECT_STATEMENT("2F000"), + INVALID_SQL_DESCRIPTOR_NAME("33000"), + INVALID_CURSOR_POSITION("34000"), + INVALID_CONDITION_NUMBER("35000"), + INVALID_TRANSACTION_TERMINATION("2D000"), + INVALID_CONNECTION_NAME("2E000"), + INVALID_AUTHORIZATION_NAME("28000"), + INVALID_COLUMN_NAME("42703"), + INVALID_COLUMN_DEFINITION("42P16"), + INVALID_CURSOR_DEFINITION("42P11"), + INVALID_DATABASE_DEFINITION("42P15"), + INVALID_FUNCTION_DEFINITION("42P13"), + INVALID_PREPARED_STATEMENT_DEFINITION("42P12"), + INVALID_SCHEMA_DEFINITION("42P14"), + INVALID_TABLE_DEFINITION("42P01"), + INVALID_OBJECT_DEFINITION("42P17"), + WITH_CHECK_OPTION_VIOLATION("44000"), + INSUFFICIENT_RESOURCES("53000"), + DISK_FULL("53100"), + OUT_OF_MEMORY("53200"), + TOO_MANY_CONNECTIONS("53300"), + CONFIGURATION_LIMIT_EXCEEDED("53400"), + PROGRAM_LIMIT_EXCEEDED("54000"), + OBJECT_NOT_IN_PREREQUISITE_STATE("55000"), + OBJECT_IN_USE("55006"), + CANT_CHANGE_RUNTIME_PARAM("55P02"), + LOCK_NOT_AVAILABLE("55P03"), + OPERATOR_INTERVENTION("57000"), + QUERY_CANCELED("57014"), + ADMIN_SHUTDOWN("57P01"), + CRASH_SHUTDOWN("57P02"), + CANNOT_CONNECT_NOW("57P03"), + DATABASE_DROPPED("57P04"), + SYSTEM_ERROR("58000"), + IO_ERROR("58030"), + UNDEFINED_FILE("58P01"), + DUPLICATE_FILE("58P02"), + SNAPSHOT_TOO_OLD("72000"), + CONFIGURATION_FILE_ERROR("F0000"), + LOCK_FILE_EXISTS("F0001"), + FDW_ERROR("HV000"), + FDW_COLUMN_NAME_NOT_FOUND("HV005"), + FDW_DYNAMIC_PARAMETER_VALUE_NEEDED("HV002"), + FDW_FUNCTION_SEQUENCE_ERROR("HV010"), + FDW_INCONSISTENT_DESCRIPTOR_INFORMATION("HV021"), + FDW_INVALID_ATTRIBUTE_VALUE("HV024"), + FDW_INVALID_COLUMN_NAME("HV007"), + FDW_INVALID_COLUMN_NUMBER("HV008"), + FDW_INVALID_DATA_TYPE("HV004"), + FDW_INVALID_DATA_TYPE_DESCRIPTORS("HV006"), + FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER("HV091"), + FDW_INVALID_HANDLE("HV00B"), + FDW_INVALID_OPTION_INDEX("HV00C"), + FDW_INVALID_OPTION_NAME("HV00D"), + FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH("HV090"), + FDW_INVALID_STRING_FORMAT("HV00A"), + FDW_INVALID_USE_OF_NULL_POINTER("HV009"), + FDW_TOO_MANY_HANDLES("HV014"), + FDW_OUT_OF_MEMORY("HV001"), + FDW_NO_SCHEMAS("HV00P"), + FDW_OPTION_NAME_NOT_FOUND("HV00J"), + FDW_REPLY_HANDLE("HV00K"), + FDW_SCHEMA_NOT_FOUND("HV00Q"), + FDW_TABLE_NOT_FOUND("HV00R"), + FDW_UNABLE_TO_CREATE_EXECUTION("HV00L"), + FDW_UNABLE_TO_CREATE_REPLY("HV00M"), + FDW_UNABLE_TO_ESTABLISH_CONNECTION("HV00N"), + PLPGSQL_ERROR("P0000"), + RAISE_EXCEPTION("P0001"), + NO_DATA_FOUND("P0002"), + TOO_MANY_ROWS("P0003"), + ASSERT_FAILURE("P0004"), + INTERNAL_ERROR("XX000"), + DATA_CORRUPTED("XX001"), + INDEX_CORRUPTED("XX002"), + STATE_NOT_DEFINED(null); + + private final String code; + private static final Map codeMap = new HashMap<>(); + static { + for (SQLState s : EnumSet.allOf(SQLState.class)) + codeMap.put(s.getCode(), s); + } + + SQLState(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public static SQLState fromCode(String sqlState) { + return codeMap.get(sqlState); + } +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java b/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java index a540a1153..a03c27867 100644 --- a/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java +++ b/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java @@ -4,6 +4,8 @@ import com.firebolt.jdbc.connection.FireboltConnectionTokens; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.exception.SQLState; + import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import net.jodah.expiringmap.ExpiringMap; @@ -51,7 +53,8 @@ public FireboltConnectionTokens getConnectionTokens(String host, FireboltPropert } catch (FireboltException e) { log.log(Level.SEVERE, "Failed to connect to Firebolt", e); String msg = ofNullable(e.getErrorMessageFromServer()).map(m -> format(ERROR_MESSAGE_FROM_SERVER, m)).orElse(format(ERROR_MESSAGE, e.getMessage())); - throw new FireboltException(msg, e); + SQLState sqlState = SQLState.fromCode(e.getSQLState()); + throw new FireboltException(msg, e, sqlState); } catch (Exception e) { log.log(Level.SEVERE, "Failed to connect to Firebolt", e); throw new FireboltException(format(ERROR_MESSAGE, e.getMessage()), e); @@ -69,7 +72,7 @@ private long getCachingDurationInSeconds(long expireInSeconds) { /** * Removes connection tokens from the cache. - * + * * @param host host * @param loginProperties the login properties linked to the tokens */ diff --git a/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java b/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java index c120e3378..a65a1ae27 100644 --- a/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java +++ b/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java @@ -3,6 +3,8 @@ import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.FireboltConnectionTokens; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.exception.SQLState; + import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -24,6 +26,7 @@ import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -118,7 +121,21 @@ void shouldThrowExceptionWhenStatusCodeIsForbidden() throws Exception { when(response.code()).thenReturn(HTTP_FORBIDDEN); when(httpClient.newCall(any())).thenReturn(call); - assertThrows(FireboltException.class, + FireboltException ex = assertThrows(FireboltException.class, + () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV)); + assertEquals(SQLState.INVALID_AUTHORIZATION_SPECIFICATION.getCode(), ex.getSQLState()); + } + + @Test + void shouldThrowExceptionWhenStatusCodeIsUnavailable() throws Exception { + Response response = mock(Response.class); + Call call = mock(Call.class); + when(call.execute()).thenReturn(response); + when(response.code()).thenReturn(HTTP_UNAVAILABLE); + when(httpClient.newCall(any())).thenReturn(call); + + FireboltException ex = assertThrows(FireboltException.class, () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV)); + assertEquals(SQLState.CONNECTION_FAILURE.getCode(), ex.getSQLState()); } } diff --git a/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java b/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java index 83e6b0160..360eac32b 100644 --- a/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java +++ b/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java @@ -138,7 +138,6 @@ private FireboltAccountRetriever mockAccountRetriever(String path, Class< @CsvSource({ HTTP_BAD_REQUEST + "," + GENERIC_ERROR_MESSAGE, HTTP_PAYMENT_REQUIRED + "," + GENERIC_ERROR_MESSAGE, - HTTP_FORBIDDEN + "," + GENERIC_ERROR_MESSAGE, HTTP_BAD_METHOD + "," + GENERIC_ERROR_MESSAGE, HTTP_NOT_ACCEPTABLE + "," + GENERIC_ERROR_MESSAGE, HTTP_PROXY_AUTH + "," + GENERIC_ERROR_MESSAGE, @@ -157,8 +156,12 @@ private FireboltAccountRetriever mockAccountRetriever(String path, Class< HTTP_VERSION + "," + GENERIC_ERROR_MESSAGE, HTTP_NOT_FOUND + "," + "Account '%s' does not exist", - HTTP_UNAVAILABLE + "," + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The engine is not running.", - HTTP_UNAUTHORIZED + "," + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The operation is not authorized" + HTTP_UNAVAILABLE + "," + + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The engine is not running.", + HTTP_FORBIDDEN + "," + + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The operation is not authorized", + HTTP_UNAUTHORIZED + "," + + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The operation is not authorized" }) void testFailedAccountDataRetrieving(int statusCode, String errorMessageTemplate) throws IOException { injectMockedResponse(httpClient, statusCode, null); diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java index 38d638603..6c05ae6ed 100644 --- a/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java +++ b/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java @@ -20,6 +20,7 @@ import com.firebolt.jdbc.connection.FireboltConnectionTokens; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.exception.SQLState; @ExtendWith(MockitoExtension.class) class FireboltAuthenticationServiceTest { @@ -80,6 +81,21 @@ void shouldGetConnectionTokenAfterRemoving() throws SQLException, IOException { @Test void shouldThrowExceptionWithServerResponseWhenAResponseIsAvailable() throws SQLException, IOException { + String randomHost = UUID.randomUUID().toString(); + Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)) + .thenThrow(new FireboltException("An error happened during authentication", 403, "INVALID PASSWORD", + SQLState.INVALID_AUTHORIZATION_SPECIFICATION)); + + FireboltException ex = assertThrows(FireboltException.class, + () -> fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); + assertEquals( + "Failed to connect to Firebolt with the error from the server: INVALID PASSWORD, see logs for more info.", + ex.getMessage()); + assertEquals(SQLState.INVALID_AUTHORIZATION_SPECIFICATION.getCode(), ex.getSQLState()); + } + + @Test + void shouldThrowExceptionNoSQLStateWithServerResponseWhenAResponseIsAvailable() throws SQLException, IOException { String randomHost = UUID.randomUUID().toString(); Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)) .thenThrow(new FireboltException("An error happened during authentication", 403, "INVALID PASSWORD")); @@ -89,6 +105,7 @@ void shouldThrowExceptionWithServerResponseWhenAResponseIsAvailable() throws SQL assertEquals( "Failed to connect to Firebolt with the error from the server: INVALID PASSWORD, see logs for more info.", ex.getMessage()); + assertEquals(null, ex.getSQLState()); } @Test @@ -100,6 +117,7 @@ void shouldThrowExceptionWithExceptionMessageWhenAResponseIsNotAvailable() throw FireboltException ex = assertThrows(FireboltException.class, () -> fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); assertEquals("Failed to connect to Firebolt with the error: NULL!, see logs for more info.", ex.getMessage()); + assertEquals(null, ex.getSQLState()); } }