use std::sync::Arc;
use jammi_ai::session::InferenceSession;
use jammi_db::catalog::channel_repo::ChannelCatalogError;
use jammi_db::error::JammiError;
use jammi_db::store::mutable::MutableTableError;
use jammi_db::trigger::TriggerError;
use jammi_db::TenantId;
use jammi_wire::{attach_error_detail, attach_trigger_detail};
use tonic::{Code, Request, Status};
use crate::grpc::session::SessionTenant;
pub fn session_tenant<T>(request: &Request<T>) -> Option<TenantId> {
request
.extensions()
.get::<SessionTenant>()
.and_then(|s| s.0)
}
pub fn session_tenant_traced<T>(request: &Request<T>) -> Option<TenantId> {
let tenant = session_tenant(request);
tracing::Span::current().record("tenant_id", tracing::field::debug(&tenant));
tenant
}
pub async fn scoped<F, Fut, T, E>(
session: &Arc<InferenceSession>,
tenant: Option<TenantId>,
f: F,
) -> Result<T, E>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
match tenant {
Some(t) => session.with_tenant_scoped(t, |_scope| f()).await,
None => f().await,
}
}
pub fn require_nonempty(value: &str, field: &str) -> Result<(), Status> {
if value.is_empty() {
Err(Status::invalid_argument(format!("{field} is required")))
} else {
Ok(())
}
}
pub fn map_engine_error(err: JammiError) -> Status {
let (code, message) = match &err {
JammiError::Source { source_id, message } => (
Code::InvalidArgument,
format!("source {source_id}: {message}"),
),
JammiError::Model { model_id, message } => (
Code::InvalidArgument,
format!("model {model_id}: {message}"),
),
JammiError::ModelNotFound { model_id } => {
(Code::NotFound, format!("model {model_id} not found"))
}
JammiError::ModelReferenced {
model_id,
referenced_by,
} => (
Code::FailedPrecondition,
format!(
"model {model_id} is still referenced by {}",
referenced_by.join(", ")
),
),
JammiError::Tenant(detail) => (Code::InvalidArgument, format!("tenant: {detail}")),
JammiError::Config(detail) => (Code::InvalidArgument, format!("config: {detail}")),
JammiError::Schema { .. } => (Code::InvalidArgument, err.to_string()),
JammiError::Eval(detail) => (Code::InvalidArgument, format!("eval: {detail}")),
JammiError::Inference(detail) => (Code::Internal, format!("inference: {detail}")),
JammiError::MutableTable(mt) => {
let code = match mt {
MutableTableError::NotFound(_) => Code::NotFound,
MutableTableError::AlreadyExists(_) => Code::AlreadyExists,
MutableTableError::InvalidId(_)
| MutableTableError::Schema(_)
| MutableTableError::MissingPrimaryKey(_)
| MutableTableError::ReservedColumn(_)
| MutableTableError::NoOrderColumn => Code::InvalidArgument,
MutableTableError::Backend(_) => Code::Internal,
};
(code, mt.to_string())
}
JammiError::ChannelCatalog(c) => {
let code = match c {
ChannelCatalogError::AlreadyExists(_)
| ChannelCatalogError::ColumnAlreadyDeclared { .. } => Code::AlreadyExists,
ChannelCatalogError::NotRegistered(_) => Code::NotFound,
ChannelCatalogError::ColumnConflict { .. } => Code::FailedPrecondition,
ChannelCatalogError::InvalidId(_) | ChannelCatalogError::InvalidColumnType(_) => {
Code::InvalidArgument
}
};
(code, c.to_string())
}
JammiError::ChannelAssembly(detail) => {
(Code::Internal, format!("channel assembly: {detail}"))
}
other => (Code::Internal, other.to_string()),
};
attach_error_detail(code, message, &err)
}
pub fn map_trigger_error(err: TriggerError) -> Status {
let (code, message) = match &err {
TriggerError::TopicNotFound(name) => (Code::NotFound, name.clone()),
TriggerError::BatchSchemaMismatch(detail) => (Code::InvalidArgument, detail.clone()),
TriggerError::SchemaConflict { topic, detail } => (
Code::FailedPrecondition,
format!("schema conflict on {topic}: {detail}"),
),
TriggerError::UnsupportedSchemaType { column, data_type } => (
Code::InvalidArgument,
format!("unsupported topic schema type for '{column}': {data_type}"),
),
TriggerError::PublishTenantMismatch {
topic,
topic_tenant,
publish_tenant,
} => (
Code::PermissionDenied,
format!(
"publish tenant mismatch on topic '{topic}': topic_tenant={topic_tenant:?}, publish_tenant={publish_tenant:?}"
),
),
TriggerError::PredicateParse(detail) | TriggerError::PredicateUnsupported(detail) => {
(Code::InvalidArgument, format!("predicate: {detail}"))
}
TriggerError::PredicateEval(detail) => (Code::Internal, format!("predicate: {detail}")),
TriggerError::OffsetEvicted(n) => {
(Code::FailedPrecondition, format!("offset {n} evicted"))
}
TriggerError::BackingTable(e) => (Code::Internal, format!("backing table: {e}")),
TriggerError::Backend(e) => (Code::Internal, format!("backend: {e}")),
TriggerError::Driver(detail) => (Code::Unavailable, format!("broker: {detail}")),
TriggerError::Catalog(detail) => (Code::Internal, format!("catalog: {detail}")),
};
attach_trigger_detail(code, message, &err)
}
#[cfg(test)]
mod tests {
use super::*;
use jammi_wire::error_from_status;
#[test]
fn model_not_found_maps_to_not_found() {
let status = map_engine_error(JammiError::ModelNotFound {
model_id: "acme/embed-mini".into(),
});
assert_eq!(status.code(), Code::NotFound);
let bad = map_engine_error(JammiError::Model {
model_id: "acme/embed-mini".into(),
message: "invalid version".into(),
});
assert_eq!(bad.code(), Code::InvalidArgument);
}
#[test]
fn model_not_found_detail_round_trips() {
let status = map_engine_error(JammiError::ModelNotFound {
model_id: "acme/embed-mini".into(),
});
match error_from_status(&status) {
JammiError::ModelNotFound { model_id } => assert_eq!(model_id, "acme/embed-mini"),
other => panic!("expected ModelNotFound to round-trip, got {other:?}"),
}
}
}