mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
fix: preserve typed catalog errors through Flight and Python (#4867)
Catalog lookup failures lose their structured resource identity at the Rust/Python SDK boundaries, leaving callers with generic errors or empty correlation fields. Decode the existing Flight SQL error metadata and preserve resource kind/name, namespace, error code, and request/job IDs in the existing error types. Direct `get_function` calls unwrap named Function or namespace failures. Generic SQL job failures remain `JobFailedError` with the typed resource error as their cause; native deferred commands correctly leave durable job IDs null. Existing unstructured SQL classifications, including `FailedPrecondition`, remain supported. Validation: focused Rust/Python regressions and a native-extension run against a real Sophon process passed all five Function/Secret resource-error cases. The final one-commit candidate passes `cargo check -p lancedb-python` and formatting; earlier runtime evidence applies after removal of an unused tuple member. Independent deployed acceptance remains pending for ENT-3373. Server companion: https://github.com/lancedb/sophon/pull/9804. Integration validation: all 14 DocLinks pass on the merge candidate with #4869. The subsequent main documentation routing change was inspected; it does not alter the typed-error paths. The inherited pin-utils cargo-deny finding is tracked in a separate dependency repair.
This commit is contained in:
@@ -310,6 +310,9 @@ fn new_py_sql_error<'py>(
|
||||
grpc_code: i32,
|
||||
metadata: &RemoteErrorMetadata,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
if sql_has_resource_error(metadata, grpc_code) {
|
||||
return new_py_sql_resource_error(py, message, request_id, grpc_code, metadata);
|
||||
}
|
||||
py.import("lancedb.remote.errors")?
|
||||
.getattr("SqlError")?
|
||||
.call1((
|
||||
@@ -322,6 +325,51 @@ fn new_py_sql_error<'py>(
|
||||
))
|
||||
}
|
||||
|
||||
fn new_py_sql_resource_error<'py>(
|
||||
py: Python<'py>,
|
||||
message: &str,
|
||||
request_id: &str,
|
||||
grpc_code: i32,
|
||||
metadata: &RemoteErrorMetadata,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let resource = metadata.resource.as_ref().unwrap();
|
||||
py.import("lancedb.remote.errors")?
|
||||
.getattr("HttpError")?
|
||||
.call1((
|
||||
message,
|
||||
request_id,
|
||||
grpc_code_to_http_status(grpc_code),
|
||||
metadata.error_code.as_deref(),
|
||||
resource.kind.as_str(),
|
||||
resource.name.as_str(),
|
||||
resource.namespace_path.clone(),
|
||||
false,
|
||||
metadata.job_id.as_deref(),
|
||||
metadata.job_request_id.as_deref(),
|
||||
))
|
||||
}
|
||||
|
||||
fn sql_has_resource_error(metadata: &RemoteErrorMetadata, grpc_code: i32) -> bool {
|
||||
metadata.resource.is_some()
|
||||
&& matches!(
|
||||
(metadata.error_code.as_deref(), grpc_code),
|
||||
// These are gRPC status codes, not HTTP status codes.
|
||||
(Some("not_found"), 5) | (Some("already_exists"), 6)
|
||||
)
|
||||
}
|
||||
|
||||
fn grpc_code_to_http_status(grpc_code: i32) -> Option<u16> {
|
||||
match grpc_code {
|
||||
3 => Some(400),
|
||||
5 => Some(404),
|
||||
6 => Some(409),
|
||||
7 => Some(403),
|
||||
9 => Some(400),
|
||||
12 => Some(501),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the typed transport classification across the Python boundary.
|
||||
// A missing status code alone also covers malformed job responses, which
|
||||
// waiting longer cannot repair.
|
||||
@@ -335,3 +383,178 @@ fn is_transport_error(err: &(dyn std::error::Error + 'static)) -> bool {
|
||||
}
|
||||
err.source().is_some_and(is_transport_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use lancedb::error::{JobFailure, RemoteResourceIdentity};
|
||||
use pyo3::types::PyAnyMethods;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generic_job_failure_retains_the_named_resource_as_its_cause() {
|
||||
Python::initialize();
|
||||
let source = LanceError::Sql {
|
||||
message: "Function 'missing' was not found".into(),
|
||||
request_id: "current-request".into(),
|
||||
grpc_code: 5,
|
||||
metadata: Box::new({
|
||||
let mut metadata = RemoteErrorMetadata::default();
|
||||
metadata.error_code = Some("not_found".into());
|
||||
metadata.job_id = Some("outer-job".into());
|
||||
metadata.job_request_id = Some("worker-request".into());
|
||||
metadata.resource = Some(RemoteResourceIdentity::new(
|
||||
"function",
|
||||
"missing",
|
||||
vec!["ns".into()],
|
||||
));
|
||||
metadata
|
||||
}),
|
||||
};
|
||||
let error = LanceError::JobFailed {
|
||||
job_id: Some("outer-job".into()),
|
||||
failure: Box::new(JobFailure {
|
||||
message: Some("Function 'missing' was not found".into()),
|
||||
traceback: None,
|
||||
sql_job_id: Some("outer-job".into()),
|
||||
source: Some(Arc::new(source)),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
let py_err = Python::attach(|_| {
|
||||
let result: std::result::Result<(), LanceError> = Err(error);
|
||||
result.infer_error().unwrap_err()
|
||||
});
|
||||
Python::attach(|py| {
|
||||
let value = py_err.value(py);
|
||||
let job_failed = py
|
||||
.import("lancedb.exceptions")
|
||||
.unwrap()
|
||||
.getattr("JobFailedError")
|
||||
.unwrap();
|
||||
assert!(value.is_instance(&job_failed).unwrap(), "{py_err:?}");
|
||||
let cause = value.getattr("__cause__").unwrap();
|
||||
let function_not_found = py
|
||||
.import("lancedb.remote.errors")
|
||||
.unwrap()
|
||||
.getattr("FunctionNotFoundError")
|
||||
.unwrap();
|
||||
assert!(
|
||||
cause.is_instance(&function_not_found).unwrap(),
|
||||
"{py_err:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("error_code")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"not_found"
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("resource_kind")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"function"
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("resource_name")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"missing"
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("namespace_path")
|
||||
.unwrap()
|
||||
.extract::<Vec<String>>()
|
||||
.unwrap(),
|
||||
["ns"]
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("status_code")
|
||||
.unwrap()
|
||||
.extract::<u16>()
|
||||
.unwrap(),
|
||||
404
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("request_id")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"current-request"
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("job_id")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"outer-job"
|
||||
);
|
||||
assert_eq!(
|
||||
cause
|
||||
.getattr("job_request_id")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"worker-request"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_catalog_lookup_is_raised_as_the_named_resource_error() {
|
||||
Python::initialize();
|
||||
for (kind, name, namespace_path, exception) in [
|
||||
(
|
||||
"function",
|
||||
"missing",
|
||||
vec!["ns".into()],
|
||||
"FunctionNotFoundError",
|
||||
),
|
||||
("namespace", "ns", Vec::new(), "NamespaceNotFoundError"),
|
||||
] {
|
||||
let mut metadata = RemoteErrorMetadata::default();
|
||||
metadata.error_code = Some("not_found".into());
|
||||
metadata.resource = Some(RemoteResourceIdentity::new(kind, name, namespace_path));
|
||||
let error = LanceError::Sql {
|
||||
message: format!("{kind} '{name}' was not found"),
|
||||
request_id: "current-request".into(),
|
||||
grpc_code: 5,
|
||||
metadata: Box::new(metadata),
|
||||
};
|
||||
let py_err = Python::attach(|_| {
|
||||
let result: std::result::Result<(), LanceError> = Err(error);
|
||||
result.infer_error().unwrap_err()
|
||||
});
|
||||
Python::attach(|py| {
|
||||
let value = py_err.value(py);
|
||||
let class = py
|
||||
.import("lancedb.remote.errors")
|
||||
.unwrap()
|
||||
.getattr(exception)
|
||||
.unwrap();
|
||||
assert!(value.is_instance(&class).unwrap(), "{py_err:?}");
|
||||
assert_eq!(
|
||||
value
|
||||
.getattr("resource_name")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
name
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user