Skip to content

Commit 9634feb

Browse files
committed
Add query id to error messages when possible
1 parent 921daa7 commit 9634feb

File tree

9 files changed

+150
-10
lines changed

9 files changed

+150
-10
lines changed

driver/api/impl/impl.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,18 @@ SQLRETURN GetStmtAttr(
505505
CASE_NUM(SQL_ATTR_RETRIEVE_DATA, SQLULEN, SQL_RD_ON);
506506
CASE_NUM(SQL_ATTR_USE_BOOKMARKS, SQLULEN, SQL_UB_OFF);
507507

508+
case SQL_CH_STMT_ATTR_LAST_QUERY_ID: {
509+
auto & query_id = statement.getQueryId();
510+
if (query_id.empty())
511+
throw SqlException("Invalid cursor state", "24000");
512+
513+
// Note that the driver always returns the query ID as a UTF-8 string, and the driver manager does not
514+
// alter or convert this value. Therefore, the user should convert the value to the expected format.
515+
// Since this is for internal use, it's simpler to keep it this way. If we decide to make it public,
516+
// we should convert the value to a SQLGUID.
517+
return fillOutputString<PTChar>(query_id, out_value, out_value_max_length, out_value_length, true);
518+
}
519+
508520
case SQL_ATTR_FETCH_BOOKMARK_PTR:
509521
case SQL_ATTR_KEYSET_SIZE:
510522
case SQL_ATTR_SIMULATE_CURSOR:

driver/driver.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,16 @@ void Driver::writeLogSessionEnd(std::ostream & stream) {
140140
}
141141
stream << " ====================" << std::endl;
142142
}
143+
144+
std::string Driver::addContextInfoToExceptionMessage(const std::string & message, SQLHANDLE handle, SQLSMALLINT handle_type) const
145+
{
146+
// Add query_id to exceptions related to statements
147+
if (handle_type == SQL_HANDLE_STMT) {
148+
auto * statement = static_cast<Statement *>(handle);
149+
const auto query_id = statement->getQueryId();
150+
if (!query_id.empty()) {
151+
return "Error while processing query " + query_id + ": " + message;
152+
}
153+
}
154+
return message;
155+
}

driver/driver.h

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class Driver
9393
virtual void onAttrChange(int attr) final override;
9494

9595
private:
96+
std::string addContextInfoToExceptionMessage(const std::string & message, SQLHANDLE handle, SQLSMALLINT handle_type) const;
97+
9698
template <typename Callable>
9799
static inline SQLRETURN doCall(Callable & callable,
98100
typename std::enable_if<
@@ -235,27 +237,31 @@ inline SQLRETURN Driver::call(Callable && callable, SQLHANDLE handle, SQLSMALLIN
235237
return doCall(callable, descendant, skip_diag);
236238
}
237239
catch (const SqlException & ex) {
238-
LOG(ex.getSQLState() << " (" << ex.what() << ")" << "[rc: " << ex.getReturnCode() << "]");
240+
auto error_message = addContextInfoToExceptionMessage(ex.what(), handle, handle_type);
241+
LOG(ex.getSQLState() << " (" << error_message << ")" << "[rc: " << ex.getReturnCode() << "]");
239242
if (!skip_diag)
240-
descendant.fillDiag(ex.getReturnCode(), ex.getSQLState(), ex.what(), 1);
243+
descendant.fillDiag(ex.getReturnCode(), ex.getSQLState(), error_message, 1);
241244
return ex.getReturnCode();
242245
}
243246
catch (const Poco::Exception & ex) {
244-
LOG("HY000 (" << ex.displayText() << ")");
247+
auto error_message = addContextInfoToExceptionMessage(ex.displayText(), handle, handle_type);
248+
LOG("HY000 (" << error_message << ")");
245249
if (!skip_diag)
246-
descendant.fillDiag(SQL_ERROR, "HY000", ex.displayText(), 1);
250+
descendant.fillDiag(SQL_ERROR, "HY000", error_message, 1);
247251
return SQL_ERROR;
248252
}
249253
catch (const std::exception & ex) {
250-
LOG("HY000 (" << ex.what() << ")");
254+
auto error_message = addContextInfoToExceptionMessage(ex.what(), handle, handle_type);
255+
LOG("HY000 (" << error_message << ")");
251256
if (!skip_diag)
252-
descendant.fillDiag(SQL_ERROR, "HY000", ex.what(), 1);
257+
descendant.fillDiag(SQL_ERROR, "HY000", error_message, 1);
253258
return SQL_ERROR;
254259
}
255260
catch (...) {
261+
auto error_message = addContextInfoToExceptionMessage("Unknown exception", handle, handle_type);
256262
LOG("HY000 (Unknown exception)");
257263
if (!skip_diag)
258-
descendant.fillDiag(SQL_ERROR, "HY000", "Unknown exception", 2);
264+
descendant.fillDiag(SQL_ERROR, "HY000", error_message, 2);
259265
return SQL_ERROR;
260266
}
261267
};

driver/platform/platform.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,7 @@
117117
#define CH_SQL_ATTR_DRIVERLOG (SQL_ATTR_TRACE + CH_SQL_OFFSET)
118118
#define CH_SQL_ATTR_DRIVERLOGFILE (SQL_ATTR_TRACEFILE + CH_SQL_OFFSET)
119119

120+
// User range is between SQL_DRIVER_STMT_ATTR_BASE (0x00004000) and 0x00007FFF, see
121+
// https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/driver-specific-data-types-descriptor-information-diagnostic
122+
#define SQL_CH_STMT_ATTR_BASE (SQL_DRIVER_STMT_ATTR_BASE + 1)
123+
#define SQL_CH_STMT_ATTR_LAST_QUERY_ID (SQL_CH_STMT_ATTR_BASE + 1)

driver/statement.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Statement::HttpRequestData Statement::prepareHttpRequest()
9999

100100
void Statement::requestNextPackOfResultSets(std::unique_ptr<ResultMutator> && mutator) {
101101
result_reader.reset();
102+
query_id.clear();
102103

103104
const auto param_set_array_size = getEffectiveDescriptor(SQL_ATTR_APP_PARAM_DESC).getAttrAs<SQLULEN>(SQL_DESC_ARRAY_SIZE, 1);
104105
if (next_param_set_idx >= param_set_array_size)
@@ -167,6 +168,13 @@ void Statement::requestNextPackOfResultSets(std::unique_ptr<ResultMutator> && mu
167168
}
168169
}
169170

171+
// response->get accepts default value as `const std::string &`
172+
// and so we cannot pass an rvalue to it (e.g.`std::string()`),
173+
// it will be deleted when the function returns.
174+
// We use a static variable that will never be deleted.
175+
static const std::string empty_query_id{};
176+
query_id = response->get("X-ClickHouse-Query-Id", empty_query_id);
177+
170178
Poco::Net::HTTPResponse::HTTPStatus status = response->getStatus();
171179
if (status != Poco::Net::HTTPResponse::HTTP_OK) {
172180
std::stringstream error_message;
@@ -355,6 +363,10 @@ ResultSet & Statement::getResultSet() {
355363
return result_reader->getResultSet();
356364
}
357365

366+
const std::string & Statement::getQueryId() const {
367+
return query_id;
368+
}
369+
358370
bool Statement::advanceToNextResultSet() {
359371
if (!is_executed)
360372
return false;
@@ -382,6 +394,7 @@ void Statement::closeCursor() {
382394
}
383395

384396
result_reader.reset();
397+
query_id.clear();
385398
in = nullptr;
386399
response.reset();
387400

driver/statement.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ class Statement
4444
/// Indicates whether there is an result set available for reading.
4545
bool hasResultSet() const;
4646

47-
/// Get the current resukt set.
47+
/// Get the current result set.
4848
ResultSet & getResultSet();
4949

50+
/// Get the current query id, or an empty string if query id is not applicable or not available.
51+
const std::string & getQueryId() const;
52+
5053
/// Make the next result set current, if any.
5154
bool advanceToNextResultSet();
5255

@@ -113,5 +116,6 @@ class Statement
113116
std::unique_ptr<Poco::Net::HTTPResponse> response;
114117
std::istream* in = nullptr;
115118
std::unique_ptr<ResultReader> result_reader;
119+
std::string query_id;
116120
std::size_t next_param_set_idx = 0;
117121
};

driver/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function (declare_odbc_test_targets libname UNICODE)
8585
performance_it.cpp
8686
type_info_it.cpp
8787
authentication_it.cpp
88+
error_handling_it.cpp
8889
)
8990

9091
if (CH_ODBC_ENABLE_CODE_COVERAGE)

driver/test/error_handling_it.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#include "driver/test/client_utils.h"
2+
#include "driver/test/client_test_base.h"
3+
#include "driver/test/result_set_reader.hpp"
4+
5+
#include <gtest/gtest.h>
6+
#include <gmock/gmock.h>
7+
8+
class ErrorHandlingTest
9+
: public ClientTestBase
10+
{
11+
public:
12+
std::string getQueryId()
13+
{
14+
char query_id_data[74] = {};
15+
SQLINTEGER query_id_len{};
16+
SQLGetStmtAttr(hstmt, SQL_CH_STMT_ATTR_LAST_QUERY_ID, query_id_data, std::size(query_id_data), &query_id_len);
17+
18+
#ifdef _win_
19+
return toUTF8(reinterpret_cast<PTChar*>(query_id_data));
20+
#else
21+
// unixODBC usually converts all strings from the driver encoding to the application encoding,
22+
// similar to how Windows behaves. However, `SQLGetStmtAttr` appears to be an exception in unixODBC,
23+
// as it passes the result in the driver encoding unchanged.
24+
// See: https://github.com/lurcher/unixODBC/issues/22
25+
//
26+
// To solve this issue and ensure the result reaches the application in the correct encoding,
27+
// we would need to implement both `SQLGetStmtAttr` and `SQLGetStmtAttrW` in the driver.
28+
// This would introduce a number of workarounds and non-uniform solutions.
29+
// Currently, there is only one string attribute, `SQL_CH_STMT_ATTR_LAST_QUERY_ID`, which is internal,
30+
// so this added complexity in the driver is not currently justified.
31+
//
32+
// Since we know that the Query ID is 36 bytes long, we can infer the encoding of the result
33+
// and re-encode it accordingly here in the test.
34+
if (query_id_len == 36)
35+
return toUTF8(query_id_data);
36+
else
37+
return toUTF8(reinterpret_cast<char16_t*>(query_id_data));
38+
#endif
39+
}
40+
};
41+
42+
/**
43+
* Given a query with a syntax error, we receive the error description with the query id.
44+
*/
45+
TEST_F(ErrorHandlingTest, ServerExceptionInBeginning)
46+
{
47+
static const size_t stream_size = 100000;
48+
auto query = fromUTF8<PTChar>("SELECT * FROM this.table.cannot.exist");
49+
50+
ODBC_CALL_ON_STMT_THROW(hstmt, SQLPrepare(hstmt, ptcharCast(query.data()), SQL_NTS));
51+
ASSERT_EQ(SQLExecute(hstmt), SQL_ERROR);
52+
auto error_message = extract_diagnostics(hstmt, SQL_HANDLE_STMT);
53+
auto query_id = getQueryId();
54+
55+
auto expect_error_message = "1:[HY000][1]Error while processing query " + query_id + ": HTTP status code: 400";
56+
ASSERT_EQ(error_message.substr(0, expect_error_message.size()), expect_error_message);
57+
}
58+
59+
/**
60+
* Given a query with an exception in the middle of the data stream, we receive the error description with the query id.
61+
* (We do not yet parse error messages, we only report "Incomplete input stream, expected at least" at the moment)
62+
*/
63+
TEST_F(ErrorHandlingTest, ServerExceptionInMiddleOfStream)
64+
{
65+
static const size_t stream_size = 100000;
66+
auto query = fromUTF8<PTChar>(
67+
"SELECT throwIf(number >= "
68+
+ std::to_string(stream_size) + ") FROM numbers("
69+
+ std::to_string(stream_size) + " + 1)");
70+
71+
ODBC_CALL_ON_STMT_THROW(hstmt, SQLPrepare(hstmt, ptcharCast(query.data()), SQL_NTS));
72+
ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecute(hstmt));
73+
74+
std::string error_message = "";
75+
SQLRETURN res = SQL_SUCCESS;
76+
while (res == SQL_SUCCESS)
77+
{
78+
res = SQLFetch(hstmt);
79+
if (res != SQL_SUCCESS)
80+
error_message = extract_diagnostics(hstmt, SQL_HANDLE_STMT);
81+
}
82+
83+
auto query_id = getQueryId();
84+
auto expect_error_message = "1:[HY000][1]Error while processing query " + query_id + ": ";
85+
ASSERT_EQ(error_message.substr(0, expect_error_message.size()), expect_error_message);
86+
}

driver/utils/amortized_istream_reader.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ class AmortizedIStreamReader
2323

2424
~AmortizedIStreamReader() {
2525
// Put back any pre-read characters, just in case...
26+
// (it should be done in reverse order)
2627
if (available() > 0) {
27-
for (std::size_t i = buffer_.size() - 1; i >= offset_; --i) {
28-
raw_stream_.putback(buffer_[i]);
28+
for (auto it = buffer_.rbegin(); it < buffer_.rend() - offset_; ++it) {
29+
raw_stream_.putback(*it);
2930
}
3031
}
3132
}

0 commit comments

Comments
 (0)