Skip to content

feat(cubesql): Universally prefix all errors#10758

Open
MazterQyou wants to merge 1 commit intomasterfrom
cubesql/error-prefixing
Open

feat(cubesql): Universally prefix all errors#10758
MazterQyou wants to merge 1 commit intomasterfrom
cubesql/error-prefixing

Conversation

@MazterQyou
Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Description of Changes Made

This PR makes an effort to universally prefix all errors with preset messages, and unwrap error onions wherever possible.

@MazterQyou MazterQyou requested review from a team as code owners April 28, 2026 10:42
@github-actions github-actions Bot added the rust Pull requests that update Rust code label Apr 28, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 28, 2026

Claude finished @MazterQyou's task in 6m 3s —— View job


Code Review: Universally prefix all errors

  • Gather context and read changed files
  • Review core error infrastructure (error.rs, compile/error.rs)
  • Review Postgres error handling changes
  • Review backend-native transport changes
  • Review test and snapshot changes
  • Post comprehensive review feedback

Summary

This is a well-structured PR that introduces a systematic error taxonomy across CubeSQL. The core idea — replacing the old 2-variant User/Internal classification with 10 specific error categories and adding uniform Display prefixes — is sound and addresses a real observability gap.

What works well

  1. Error onion unwrapping: The new From<DataFusionError> and From<ArrowError> implementations properly unwrap nested error types (Plan, Execution, SQL, NotImplemented, etc.) into the correct CubeSQL categories instead of flattening everything to Internal. This preserves valuable context.

  2. ContinueWait as a typed variant: Replacing fragile e.message.to_lowercase().contains("continue wait") string checks with a proper ContinueWait enum variant is a significant reliability improvement.

  3. Bidirectional From conversions: Adding From<CubeError> for CompilationError alongside the existing reverse direction enables the ? operator to work naturally, eliminating many .map_err(|e| CompilationError::internal(e.to_string())) wrappers that were flattening error context.

  4. meta() accessor: The CubeErrorCauseType::meta() helper cleanly centralizes the metadata extraction pattern.

  5. Postgres error code mapping consolidation: The old df_to_error_response/arrow_to_error_response recursive unwrapping chain is replaced by converting to CubeError first and mapping cause types to PG codes. Much cleaner.

  6. Error reclassification: Correct fixes like CubeError::userCubeError::internal for transport parsing failures, JS bridge deserialization errors, and "unexpected response" errors. These are genuinely internal issues, not user-caused.

Issues & suggestions

Medium priority:

  • http_error.rs catch-all hides user-actionable errors — The _ => "Internal Server Error" catch-all now covers SQLParser, Planning, and Unsupported errors which contain user-actionable information. If this HTTP gateway is user-facing, consider allowing these categories to expose their messages. See inline comment.

  • PG error code for Planning errorsSyntaxErrorOrAccessRuleViolation (42000) may not be the best fit for all planning errors (e.g., join validation failures, missing cube members). See inline comment.

Low priority / nits:

  • Variant naming conventionRESTAPI and SQLParser don't follow Rust PascalCase convention. See inline comment.

  • Boilerplate growth — Each CompilationError method (PartialEq, to_backtrace, message, with_message, with_meta) repeats the full variant list. A macro could reduce this. See inline comment.

  • Minor double-descriptionrust_decimal::Error formats as "Internal Error: Decimal Error: ...". See inline comment.

Testing

The test changes look correct:

  • Error message assertions updated to match new prefix format (e.g., "Planning Error: ..." instead of "Error during planning: SQLCompilationError: Internal: ...")
  • Snapshot files updated consistently
  • The message() method continues to return the raw message without prefix, while to_string()/Display includes the prefix — test assertions use the right one

Security

No security concerns. The http_error.rs catch-all correctly hides non-User error details from HTTP responses, preventing information leakage.

Overall

Solid improvement to error handling infrastructure. The error taxonomy is well-thought-out and the conversion pipelines are correct. The main risk area is ensuring downstream consumers (logs, HTTP responses, PG wire protocol) get the right level of detail — the inline comments highlight the specific spots to verify.

Comment thread rust/cubesql/cubesql/src/error.rs
}

impl From<rust_decimal::Error> for CubeError {
fn from(v: rust_decimal::Error) -> Self {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: double-description in formatted message — This produces CubeError::internal("Decimal Error: ..."), which when displayed via CubeError::Display becomes "Internal Error: Decimal Error: ...". The nested prefix reads a bit awkwardly. Consider either:

  • Dropping the "Decimal Error: " prefix and relying on the Internal Error: prefix from Display, or
  • Using a dedicated error variant if decimal errors need distinct handling

This is very minor and non-blocking.

Comment on lines 68 to +105

fn cube_to_error_response(e: &CubeError) -> protocol::ErrorResponse {
let message = e.to_string();
// Remove `Error: ` prefix that can come from JS
let message = if let Some(message) = message.strip_prefix("Error: ") {
message.to_string()
} else {
message
};
protocol::ErrorResponse::error(protocol::ErrorCode::InternalError, message)
}

fn df_to_error_response(e: &DataFusionError) -> protocol::ErrorResponse {
match e {
DataFusionError::ArrowError(arrow_err) => {
return Self::arrow_to_error_response(arrow_err);
fn cube_to_error_response(e: CubeError) -> protocol::ErrorResponse {
match e.cause {
CubeErrorCauseType::User(_) => protocol::ErrorResponse::error(
protocol::ErrorCode::InvalidSqlStatement,
e.to_string(),
),
CubeErrorCauseType::Internal(_) => {
protocol::ErrorResponse::error(protocol::ErrorCode::InternalError, e.to_string())
}
DataFusionError::External(err) => {
if let Some(cube_err) = err.downcast_ref::<CubeError>() {
return Self::cube_to_error_response(cube_err);
}
CubeErrorCauseType::RESTAPI(_) => {
protocol::ErrorResponse::error(protocol::ErrorCode::SystemError, e.to_string())
}
_ => {}
}
protocol::ErrorResponse::error(
protocol::ErrorCode::InternalError,
format!("Post-processing Error: {}", e),
)
}

fn arrow_to_error_response(e: &ArrowError) -> protocol::ErrorResponse {
match e {
ArrowError::ExternalError(err) => {
if let Some(df_err) = err.downcast_ref::<DataFusionError>() {
return Self::df_to_error_response(df_err);
}
if let Some(cube_err) = err.downcast_ref::<CubeError>() {
return Self::cube_to_error_response(cube_err);
}
CubeErrorCauseType::SQLParser(_) => {
protocol::ErrorResponse::error(protocol::ErrorCode::SyntaxError, e.to_string())
}
CubeErrorCauseType::Unsupported(_) => protocol::ErrorResponse::error(
protocol::ErrorCode::FeatureNotSupported,
e.to_string(),
),
CubeErrorCauseType::Planning(_) => protocol::ErrorResponse::error(
protocol::ErrorCode::SyntaxErrorOrAccessRuleViolation,
e.to_string(),
),
CubeErrorCauseType::PostProcessing(_) => {
protocol::ErrorResponse::error(protocol::ErrorCode::DataException, e.to_string())
}
CubeErrorCauseType::Rewrite(_) => {
protocol::ErrorResponse::error(protocol::ErrorCode::InternalError, e.to_string())
}
CubeErrorCauseType::DatabaseExecution(_) => {
protocol::ErrorResponse::error(protocol::ErrorCode::SystemError, e.to_string())
}
_ => {}
CubeErrorCauseType::ContinueWait => {
// Should never happen
protocol::ErrorResponse::error(
protocol::ErrorCode::SqlStatementNotYetComplete,
e.to_string(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good refactor — Consolidating the error-to-PG-code mapping into a single cube_to_error_response based on CubeErrorCauseType is much cleaner than the old recursive df_to_error_response / arrow_to_error_response chain that tried to unwrap errors manually.

A few observations on the PG error code mappings:

  • PlanningSyntaxErrorOrAccessRuleViolation (42000): This is the class-level code. Cube planning errors can be broader than syntax issues (e.g., join validation failures). Consider whether InternalError might be more accurate for some planning failures.
  • UserInvalidSqlStatement (26000): This PG code specifically means "invalid SQL statement name" (prepared statement naming). This mapping predates this PR, but if you're touching error codes anyway, 42000 might be semantically closer for general user errors.
  • ContinueWaitSqlStatementNotYetComplete (03000): Creative use of this code. The comment "Should never happen" is reassuring — worth adding a log::warn! if it does trigger unexpectedly.

Comment thread rust/cubesql/cubesql/src/compile/query_engine.rs
Comment on lines 82 to 120
CompilationError::User(right, _) => left == right,
_ => false,
},
CompilationError::RESTAPI(left, _) => match other {
CompilationError::RESTAPI(right, _) => left == right,
_ => false,
},
CompilationError::SQLParser(left, _) => match other {
CompilationError::SQLParser(right, _) => left == right,
_ => false,
},
CompilationError::Unsupported(left, _) => match other {
CompilationError::Unsupported(right, _) => left == right,
_ => false,
},
CompilationError::Planning(left, _) => match other {
CompilationError::Planning(right, _) => left == right,
_ => false,
},
CompilationError::PostProcessing(left, _) => match other {
CompilationError::PostProcessing(right, _) => left == right,
_ => false,
},
CompilationError::Rewrite(left, _) => match other {
CompilationError::Rewrite(right, _) => left == right,
_ => false,
},
CompilationError::DatabaseExecution(left, _) => match other {
CompilationError::DatabaseExecution(right, _) => left == right,
_ => false,
},
CompilationError::Fatal(left, _) => match other {
CompilationError::Fatal(right, _) => left == right,
_ => false,
},
CompilationError::ContinueWait => matches!(other, CompilationError::ContinueWait),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: consider a macro or helper for PartialEq — This block is very repetitive. Each new variant adds 3 lines of identical structure. A macro like:

macro_rules! match_variant_eq {
    ($self:expr, $other:expr, $($Variant:ident),+) => {
        match ($self, $other) {
            $( (CompilationError::$Variant(left, ..), CompilationError::$Variant(right, ..)) => left == right, )+
            (CompilationError::ContinueWait, CompilationError::ContinueWait) => true,
            _ => false,
        }
    }
}

could reduce this significantly. Same applies to to_backtrace(), to_backtrace_mut(), message(), with_message(), and with_meta(). Not blocking — just noting that the boilerplate will grow linearly with each new error variant.

Comment on lines 463 to 465
if let Err(e) = &result {
if e.message.to_lowercase().contains("continue wait") {
if throw_continue_wait {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: eliminates fragile string matching — The old code checked e.message.to_lowercase().contains("continue wait") to detect continue-wait signals. Using a dedicated CubeError::continue_wait() with a ContinueWait cause type is much more robust and self-documenting.

Note: there are still string-based "continue wait" checks in this file (lines 463 and 612). These handle errors coming from the JS side where you can't control the error type. Worth a follow-up comment explaining why these remain string-based (i.e., JS bridge limitation).

Comment thread rust/cubesql/cubesql/src/compile/engine/df/scan.rs
Comment thread rust/cubesql/cubesql/src/error.rs
Comment on lines 43 to 53
Self {
code: match error.cause {
CubeErrorCauseType::User(_) => code,
CubeErrorCauseType::Internal(_) => {
HttpErrorCode::StatusCode(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
}
_ => HttpErrorCode::StatusCode(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
},
message: match error.cause {
CubeErrorCauseType::User(_) => error.message,
CubeErrorCauseType::Internal(_) => "Internal Server Error".to_string(),
_ => "Internal Server Error".to_string(),
},
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct change — The catch-all _ => is necessary now that CubeErrorCauseType has many more variants. All non-User errors correctly return HTTP 500 with a generic message, preventing internal error details from leaking to HTTP clients.

Note: this means new error types like SQLParser, Planning, Unsupported etc. will all show "Internal Server Error" to HTTP callers, even though some (like SQLParser) are clearly user-caused. If this gateway is user-facing, you may want to expand which cause types show their real message (e.g., SQLParser, Unsupported, Planning could arguably be safe to expose).

Comment thread rust/cubesql/cubesql/src/sql/postgres/shim.rs
Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
@MazterQyou MazterQyou force-pushed the cubesql/error-prefixing branch from 4eec296 to 926c03e Compare April 28, 2026 16:44
@github-actions github-actions Bot added the javascript Pull requests that update Javascript code label Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant