Skip to content

Commit 60b69ea

Browse files
Copilotkrlmlrgithub-actions[bot]
authored
feat: Add rich ErrorData-based error handling with structured error information (#1479)
Co-authored-by: krlmlr <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Kirill Müller <[email protected]> Co-authored-by: Kirill Müller <[email protected]>
1 parent 495e9f0 commit 60b69ea

20 files changed

+287
-95
lines changed

R/duckdb.R

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,52 @@
55
#' @keywords internal
66
"_PACKAGE"
77
NULL
8+
9+
# Internal error function for C++ layer
10+
rapi_error <- function(context, message, error_type = NULL, raw_message = NULL, extra_info = NULL) {
11+
if (is.null(error_type) && is.null(raw_message) && is.null(extra_info)) {
12+
extra_text <- ""
13+
} else {
14+
extra_text <- paste0(
15+
" (",
16+
if (!is.null(error_type)) paste0("error_type: ", error_type, "; ") else "",
17+
if (!is.null(raw_message)) paste0("raw_message: ", raw_message, "; ") else "",
18+
if (!is.null(extra_info)) paste0(paste0(names(extra_info), ": ", extra_info, collapse = "; ")) else "",
19+
")"
20+
)
21+
}
22+
23+
stop(paste0(context, ": ", message, extra_text), call. = FALSE)
24+
}
25+
26+
# rlang error function (will be conditionally replaced in .onLoad)
27+
rapi_error_rlang <- function(context, message, error_type = NULL, raw_message = NULL, extra_info = NULL) {
28+
# Avoid initializing NULL fields
29+
fields <- list()
30+
fields$context <- context
31+
fields$error_type <- error_type
32+
fields$raw_message <- raw_message
33+
fields$extra_info <- extra_info
34+
35+
# Create error message with context
36+
error_parts <- c(message, i = paste0("Context: ", context))
37+
38+
# Add error type if available
39+
if (!is.null(error_type)) {
40+
error_parts <- c(error_parts, i = paste0("Error type: ", error_type))
41+
}
42+
43+
# Add raw message if different from processed message
44+
if (!is.null(raw_message) && raw_message != message) {
45+
error_parts <- c(error_parts, i = paste0("Raw message: ", raw_message))
46+
}
47+
48+
# Add extra info if available
49+
if (length(extra_info) > 0) {
50+
info_text <- paste0(names(extra_info), ": ", extra_info)
51+
names(info_text) <- rep_len("i", length(info_text))
52+
error_parts <- c(error_parts, info_text)
53+
}
54+
55+
rlang::abort(error_parts, class = "duckdb_error", !!!fields)
56+
}

R/zzz.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
if (requireNamespace("rlang", quietly = TRUE)) {
1414
is_interactive <<- rlang::is_interactive
1515
local_interactive <<- rlang::local_interactive
16+
rapi_error <<- rapi_error_rlang
1617
} else {
1718
rethrow_restore()
19+
# Overwrite rapi_error with base version when rlang is not available
1820
}
1921

2022
invisible()

src/connection.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ static void SetDefaultConfigArguments(ClientContext &context) {
5656

5757
[[cpp11::register]] duckdb::conn_eptr_t rapi_connect(duckdb::db_eptr_t dual, duckdb::ConvertOpts convert_opts) {
5858
if (!dual || !dual.get()) {
59-
cpp11::stop("rapi_connect: Invalid database reference");
59+
rapi_error_with_context("rapi_connect", "Invalid database reference");
6060
}
6161
auto db = dual->get();
6262
if (!db || !db->db) {
63-
cpp11::stop("rapi_connect: Database already closed");
63+
rapi_error_with_context("rapi_connect", "Database already closed");
6464
}
6565

6666
auto conn_wrapper = make_uniq<ConnWrapper>(std::move(db), std::move(convert_opts));

src/convert.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@ ConvertOpts::TzOutConvert string_to_tz_out_convert(const std::string &str) {
1111
return ConvertOpts::TzOutConvert::WITH;
1212
if (str == "force")
1313
return ConvertOpts::TzOutConvert::FORCE;
14-
cpp11::stop("Invalid tz_out_convert value: %s", str.c_str());
14+
rapi_error_with_context("string_to_tz_out_convert", "Invalid tz_out_convert value: " + str);
1515
}
1616

1717
ConvertOpts::BigIntType string_to_bigint_type(const std::string &str) {
1818
if (str == "numeric")
1919
return ConvertOpts::BigIntType::NUMERIC;
2020
if (str == "integer64")
2121
return ConvertOpts::BigIntType::INTEGER64;
22-
cpp11::stop("Invalid bigint value: %s", str.c_str());
22+
rapi_error_with_context("string_to_bigint_type", "Invalid bigint value: " + str);
2323
}
2424

2525
ConvertOpts::ArrayConversion string_to_array_conversion(const std::string &str) {
2626
if (str == "none")
2727
return ConvertOpts::ArrayConversion::NONE;
2828
if (str == "matrix")
2929
return ConvertOpts::ArrayConversion::MATRIX;
30-
cpp11::stop("Invalid array value: %s", str.c_str());
30+
rapi_error_with_context("string_to_array_conversion", "Invalid array value: " + str);
3131
}
3232

3333
ConvertOpts::ArrowConversion bool_to_arrow_conversion(bool use_arrow) {

src/database.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ static bool CastRstringToVarchar(Vector &source, Vector &result, idx_t count, Ca
4040
try {
4141
config.SetOptionByName(key, Value(val));
4242
} catch (std::exception &e) {
43-
cpp11::stop("rapi_startup: Failed to set configuration option: %s", e.what());
43+
rapi_error_with_context("rapi_startup", e);
4444
}
4545
}
4646

@@ -70,7 +70,7 @@ static bool CastRstringToVarchar(Vector &source, Vector &result, idx_t count, Ca
7070
function.global_initialization = TableFunctionInitialization::INITIALIZE_ON_SCHEDULE;
7171
}
7272
} catch (std::exception &e) {
73-
cpp11::stop("rapi_startup: Failed to open database: %s", e.what());
73+
rapi_error_with_context("rapi_startup", e);
7474
}
7575
D_ASSERT(wrapper->db);
7676

@@ -98,22 +98,22 @@ static bool CastRstringToVarchar(Vector &source, Vector &result, idx_t count, Ca
9898

9999
[[cpp11::register]] bool rapi_lock(duckdb::db_eptr_t dual) {
100100
if (!dual || !dual.get()) {
101-
cpp11::stop("rapi_lock: Invalid database reference");
101+
rapi_error_with_context("rapi_lock", "Invalid database reference");
102102
}
103103
dual->lock();
104104
return dual->has();
105105
}
106106

107107
[[cpp11::register]] void rapi_unlock(duckdb::db_eptr_t dual) {
108108
if (!dual || !dual.get()) {
109-
cpp11::stop("rapi_unlock: Invalid database reference");
109+
rapi_error_with_context("rapi_unlock", "Invalid database reference");
110110
}
111111
dual->unlock();
112112
}
113113

114114
[[cpp11::register]] bool rapi_is_locked(duckdb::db_eptr_t dual) {
115115
if (!dual || !dual.get()) {
116-
cpp11::stop("rapi_is_locked: Invalid database reference");
116+
rapi_error_with_context("rapi_is_locked", "Invalid database reference");
117117
}
118118
return dual->is_locked();
119119
}

src/include/rapi.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@
1111
#include "duckdb/common/unordered_map.hpp"
1212
#include "duckdb/parser/tableref/table_function_ref.hpp"
1313
#include "duckdb/common/mutex.hpp"
14+
#include "duckdb/common/error_data.hpp"
1415

1516
#include "convert.hpp"
1617

1718
#if defined(R_VERSION) && R_VERSION >= R_Version(4, 3, 0)
1819
#define R_HAS_ALTLIST
1920
#endif
2021

22+
// Helper functions to communicate errors via R's stop() function with context information
23+
[[noreturn]] void rapi_error_with_context(const std::string &context, const std::string &message);
24+
[[noreturn]] void rapi_error_with_context(const std::string &context, const std::exception &e);
25+
[[noreturn]] void rapi_error_with_context(const std::string &context, const duckdb::ErrorData &error_data);
26+
2127
namespace duckdb {
2228

2329
typedef unordered_map<std::string, cpp11::list> arrow_scans_t;
@@ -39,7 +45,7 @@ class DualWrapper {
3945
}
4046
DualWrapper(DualWrapper *dual) : precious_(dual->get()) {
4147
if (!precious_) {
42-
cpp11::stop("dual is already released");
48+
rapi_error_with_context("DualWrapper", "dual is already released");
4349
}
4450
}
4551
~DualWrapper() {

src/register.cpp

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ using namespace duckdb;
1515
[[cpp11::register]] void rapi_register_df(duckdb::conn_eptr_t conn, std::string name, cpp11::data_frame value,
1616
duckdb::ConvertOpts convert_opts, bool overwrite) {
1717
if (!conn || !conn.get() || !conn->conn) {
18-
cpp11::stop("rapi_register_df: Invalid connection");
18+
rapi_error_with_context("rapi_register_df", "Invalid connection");
1919
}
2020
if (name.empty()) {
21-
cpp11::stop("rapi_register_df: Name cannot be empty");
21+
rapi_error_with_context("rapi_register_df", "Name cannot be empty");
2222
}
2323
if (value.ncol() < 1) {
24-
cpp11::stop("rapi_register_df: Data frame with at least one column required");
24+
rapi_error_with_context("rapi_register_df", "Data frame with at least one column required");
2525
}
2626

2727
ScopedInterruptHandler signal_handler(conn->conn->context);
@@ -38,7 +38,7 @@ using namespace duckdb;
3838

3939
static_cast<cpp11::sexp>(conn).attr("_registered_df_" + name) = value;
4040
} catch (std::exception &e) {
41-
cpp11::stop("rapi_register_df: Failed to register data frame: %s", e.what());
41+
rapi_error_with_context("rapi_register_df", e);
4242
}
4343
}
4444

@@ -55,7 +55,7 @@ using namespace duckdb;
5555
signal_handler.HandleInterrupt();
5656

5757
if (res->HasError()) {
58-
cpp11::stop("%s", res->GetError().c_str());
58+
rapi_error_with_context("rapi_unregister_df", res->GetError());
5959
}
6060
}
6161

@@ -279,10 +279,10 @@ unique_ptr<TableRef> duckdb::ArrowScanReplacement(ClientContext &context, Replac
279279
[[cpp11::register]] void rapi_register_arrow(duckdb::conn_eptr_t conn, std::string name, cpp11::list export_funs,
280280
cpp11::sexp valuesexp) {
281281
if (!conn || !conn.get() || !conn->conn) {
282-
cpp11::stop("rapi_register_arrow: Invalid connection");
282+
rapi_error_with_context("rapi_register_arrow", "Invalid connection");
283283
}
284284
if (name.empty()) {
285-
cpp11::stop("rapi_register_arrow: Name cannot be empty");
285+
rapi_error_with_context("rapi_register_arrow", "Name cannot be empty");
286286
}
287287

288288
auto stream_factory =
@@ -297,7 +297,8 @@ unique_ptr<TableRef> duckdb::ArrowScanReplacement(ClientContext &context, Replac
297297
auto &arrow_scans = conn->db->arrow_scans;
298298

299299
for (auto e = arrow_scans.find(name); e != arrow_scans.end(); ++e) {
300-
cpp11::stop("rapi_register_arrow: Arrow table '%s' already registered", name.c_str());
300+
std::string error_msg = "Arrow table '" + name + "' already registered";
301+
rapi_error_with_context("rapi_register_arrow", error_msg);
301302
}
302303

303304
arrow_scans[name] = state_list;

src/reltoaltrep.cpp

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,18 @@ void RelToAltrep::Initialize(DllInfo *dll) {
9090
template <class T>
9191
static T *GetFromExternalPtr(SEXP x) {
9292
if (!x) {
93-
cpp11::stop("GetFromExternalPtr: need a SEXP pointer");
93+
rapi_error_with_context("GetFromExternalPtr", "need a SEXP pointer");
9494
}
9595
if (!ALTREP(x)) {
96-
cpp11::stop("GetFromExternalPtr: not an ALTREP");
96+
rapi_error_with_context("GetFromExternalPtr", "not an ALTREP");
9797
}
9898
auto ptr = R_altrep_data1(x);
9999
if (TYPEOF(ptr) != EXTPTRSXP) {
100-
cpp11::stop("GetFromExternalPtr: data1 is not an external pointer");
100+
rapi_error_with_context("GetFromExternalPtr", "data1 is not an external pointer");
101101
}
102102
auto wrapper = (T *)R_ExternalPtrAddr(ptr);
103103
if (!wrapper) {
104-
cpp11::stop("GetFromExternalPtr: This looks like it has been freed");
104+
rapi_error_with_context("GetFromExternalPtr", "This looks like it has been freed");
105105
}
106106
return wrapper;
107107
}
@@ -120,12 +120,12 @@ bool AltrepRelationWrapper::HasQueryResult() const {
120120

121121
MaterializedQueryResult *AltrepRelationWrapper::GetQueryResult() {
122122
if (!mat_error.empty()) {
123-
cpp11::stop(mat_error);
123+
rapi_error_with_context("GetQueryResult", mat_error);
124124
}
125125

126126
if (!mat_result) {
127127
if (n_cells == 0) {
128-
cpp11::stop("Materialization is disabled, use collect() or as_tibble() to materialize.");
128+
rapi_error_with_context("GetQueryResult", "Materialization is disabled, use `collect()` or `as_tibble()` to materialize.");
129129
}
130130

131131
auto materialize_callback = Rf_GetOption1(RStrings::get().materialize_callback_sym);
@@ -153,7 +153,7 @@ MaterializedQueryResult *AltrepRelationWrapper::GetQueryResult() {
153153
Materialize();
154154

155155
if (!mat_error.empty()) {
156-
cpp11::stop(mat_error);
156+
rapi_error_with_context("GetQueryResult", mat_error);
157157
}
158158

159159
// FIXME: Use std::experimental::scope_exit
@@ -204,7 +204,7 @@ void AltrepRelationWrapper::Materialize() {
204204
auto local_mat_res = (MaterializedQueryResult *)local_res.get();
205205
if (local_mat_res->RowCount() > max_rows) {
206206
mat_error = duckdb_fmt::format(
207-
"Materialization would result in more than {} rows. Use collect() or as_tibble() to materialize.",
207+
"Materialization would result in more than {} rows. Use `collect()` or `as_tibble()` to materialize.",
208208
max_rows);
209209
return;
210210
}
@@ -322,7 +322,7 @@ void *RelToAltrep::DoRownamesDataptrGet(SEXP x) {
322322
auto rownames_wrapper = AltrepRownamesWrapper::Get(x);
323323
auto row_count = rownames_wrapper->rel->GetQueryResult()->RowCount();
324324
if (row_count > (idx_t)NumericLimits<int32_t>::Maximum()) {
325-
cpp11::stop("Integer overflow for row.names attribute");
325+
rapi_error_with_context("altrep_rownames_integer_Elt", "Integer overflow for row.names attribute");
326326
}
327327
rownames_wrapper->rowlen_data[1] = -row_count;
328328
return rownames_wrapper->rowlen_data;
@@ -386,21 +386,21 @@ static R_altrep_class_t LogicalTypeToAltrepType(const LogicalType &type, const d
386386
#endif
387387

388388
default:
389-
cpp11::stop("LogicalTypeToAltrepType: Column `%s` has no type for altrep: %s", name.c_str(),
390-
type.ToString().c_str());
389+
std::string error_msg = "Column `" + name + "` has no type for altrep: " + type.ToString();
390+
rapi_error_with_context("LogicalTypeToAltrepType", error_msg);
391391
}
392392
}
393393

394394
size_t DoubleToSize(double d) {
395395
if (d < 0) {
396-
cpp11::stop("rel_to_altrep: Negative size");
396+
rapi_error_with_context("DoubleToSize", "Negative size");
397397
}
398398
if (!std::isfinite(d)) {
399399
// Return maximum size_t for Inf
400400
return MAX_SIZE_T;
401401
}
402402
if (d >= (double)MAX_SIZE_T) {
403-
cpp11::stop("rel_to_altrep: Size overflow");
403+
rapi_error_with_context("DoubleToSize", "Size overflow");
404404
}
405405
return (size_t)d;
406406
}
@@ -462,7 +462,7 @@ size_t DoubleToSize(double d) {
462462
shared_ptr<AltrepRelationWrapper> rapi_rel_wrapper_from_altrep_df(SEXP df, bool strict, bool allow_materialized) {
463463
if (!Rf_inherits(df, "data.frame")) {
464464
if (strict) {
465-
cpp11::stop("rapi_rel_from_altrep_df: Not a data.frame");
465+
rapi_error_with_context("rapi_rel_from_altrep_df", "Not a data.frame");
466466
} else {
467467
return nullptr;
468468
}
@@ -471,7 +471,7 @@ shared_ptr<AltrepRelationWrapper> rapi_rel_wrapper_from_altrep_df(SEXP df, bool
471471
auto row_names = get_attrib(df, R_RowNamesSymbol);
472472
if (row_names == R_NilValue || !ALTREP(row_names)) {
473473
if (strict) {
474-
cpp11::stop("rapi_rel_from_altrep_df: Not a 'special' data.frame, row names are not ALTREP");
474+
rapi_error_with_context("rapi_rel_from_altrep_df", "Not a 'special' data.frame, row names are not ALTREP");
475475
} else {
476476
return nullptr;
477477
}
@@ -480,7 +480,7 @@ shared_ptr<AltrepRelationWrapper> rapi_rel_wrapper_from_altrep_df(SEXP df, bool
480480
auto altrep_data = R_altrep_data1(row_names);
481481
if (TYPEOF(altrep_data) != EXTPTRSXP) {
482482
if (strict) {
483-
cpp11::stop("rapi_rel_from_altrep_df: Not our 'special' data.frame, data1 is not external pointer");
483+
rapi_error_with_context("rapi_rel_from_altrep_df", "Not our 'special' data.frame, data1 is not external pointer");
484484
} else {
485485
return nullptr;
486486
}
@@ -489,7 +489,7 @@ shared_ptr<AltrepRelationWrapper> rapi_rel_wrapper_from_altrep_df(SEXP df, bool
489489
auto tag = R_ExternalPtrTag(altrep_data);
490490
if (tag != RStrings::get().duckdb_row_names_sym) {
491491
if (strict) {
492-
cpp11::stop("rapi_rel_from_altrep_df: Not our 'special' data.frame, tag missing");
492+
rapi_error_with_context("rapi_rel_from_altrep_df", "Not our 'special' data.frame, tag missing");
493493
} else {
494494
return nullptr;
495495
}

0 commit comments

Comments
 (0)