Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions sf_core/src/apis/database_driver_v1/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,96 @@ pub enum ApiError {
source: std::io::Error,
},
}

/// Wrapper-neutral structured metadata extracted from an [`ApiError`].
///
/// Fields remain `None` when the underlying error does not carry that
/// diagnostic. Wrappers can translate the values into their native error
/// types without matching sf_core's internal error variants.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ErrorDiagnostics {
/// Snowflake's numeric server error code.
pub vendor_code: Option<i32>,
/// SQLSTATE supplied by the server or inferred from the vendor code when
/// sf_core has a canonical mapping. Server values are returned verbatim.
pub sql_state: Option<String>,
/// Server-assigned Snowflake query ID.
pub query_id: Option<String>,
/// Client-generated request ID used to submit the query.
pub request_id: Option<String>,
}

impl ApiError {
/// Returns structured diagnostics suitable for any language or driver wrapper.
///
/// The server-provided SQLSTATE is authoritative. When it is absent for a
/// query error, sf_core falls back to its vendor-code mapping. Known login
/// credential failures use the authorization SQLSTATE (`28000`). No
/// diagnostic is inferred from human-readable message text.
#[must_use]
pub fn diagnostics(&self) -> ErrorDiagnostics {
let (vendor_code, sql_state) = match self {
Self::Query { source, .. } => match source.as_ref() {
RestError::QueryFailed {
code, sql_state, ..
} => (*code, sql_state.clone()),
RestError::AsyncQuery {
source: crate::rest::snowflake::error::SfError::SnowflakeBody { code, .. },
..
} => (Some(*code), None),
_ => (None, None),
},
Self::Login { source, .. } => match source.as_ref() {
RestError::LoginError { code, .. }
if *code != crate::rest::snowflake::GS_CODE_UNAVAILABLE =>
{
(
Some(*code),
crate::rest::snowflake::CREDENTIAL_REJECTION_GS_CODES
.contains(code)
.then(|| {
crate::rest::snowflake::SQLSTATE_AUTHORIZATION_FAILURE.to_owned()
}),
)
}
_ => (None, None),
},
_ => (None, None),
};
let sql_state = sql_state.or_else(|| {
vendor_code
.and_then(crate::rest::snowflake::sql_state::sql_state_from_code)
.map(str::to_owned)
});

let (query_id, request_id) = match self {
Self::Query { source, .. } => match source.as_ref() {
RestError::QueryFailed {
query_id,
request_id,
..
} => (
query_id.clone(),
request_id.as_ref().map(ToString::to_string),
),
RestError::AsyncQuery {
query_id,
request_id,
..
} => (
query_id.as_ref().map(ToString::to_string),
request_id.as_ref().map(ToString::to_string),
),
_ => (None, None),
},
_ => (None, None),
};

ErrorDiagnostics {
vendor_code,
sql_state,
query_id,
request_id,
}
}
}
2 changes: 1 addition & 1 deletion sf_core/src/apis/database_driver_v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub use crate::config::settings::Setting;
pub use crate::handle_manager::Handle;
pub use async_query_registry::AsyncQueryRegistry;
pub use connection::{Connection, ConnectionInfo, RefreshContext, with_valid_session};
pub use error::ApiError;
pub use error::{ApiError, ErrorDiagnostics};
pub use get_objects::{
ColumnDescriptor, DEPTH_CATALOGS, DEPTH_COLUMNS, DEPTH_DB_SCHEMAS, DEPTH_TABLES,
FIELD_CATALOG_DB_SCHEMAS, FIELD_CATALOG_NAME, FIELD_COLUMN_BYTE_LENGTH,
Expand Down
Loading
Loading