Fallback dashboard aggregate reads on schema mismatch

This commit is contained in:
fawney19
2026-05-22 17:52:36 +08:00
parent ef04f4b0fb
commit 6ef6cbade2
2 changed files with 108 additions and 8 deletions

View File

@@ -175,6 +175,38 @@ fn split_dashboard_hourly_aggregate_range(
}
}
fn dashboard_aggregate_schema_mismatch_message(message: &str) -> bool {
let message = message.to_ascii_lowercase();
let references_dashboard_aggregate = [
"stats_summary",
"stats_daily",
"stats_user_daily",
"stats_daily_model_provider",
"stats_user_daily_model_provider",
"cutoff_date",
"effective_input_tokens",
"total_input_context",
"response_time_sum_ms",
"response_time_samples",
]
.iter()
.any(|pattern| message.contains(pattern));
if !references_dashboard_aggregate {
return false;
}
message.contains("does not exist")
|| message.contains("unknown column")
|| message.contains("no column named")
|| message.contains("error occurred while decoding column")
|| message.contains("is not compatible with sql type")
|| message.contains("unexpected null")
}
fn dashboard_should_fallback_to_raw_on_aggregate_error(err: &DataLayerError) -> bool {
dashboard_aggregate_schema_mismatch_message(&err.to_string())
}
fn absorb_dashboard_summary(
target: &mut StoredUsageDashboardSummary,
part: &StoredUsageDashboardSummary,
@@ -4065,7 +4097,20 @@ ORDER BY created_at_unix_secs ASC, group_id ASC, usage_id ASC
&self,
query: &UsageDashboardSummaryQuery,
) -> Result<StoredUsageDashboardSummary, DataLayerError> {
let Some(cutoff_utc) = self.read_stats_daily_cutoff_date().await? else {
let cutoff_utc = match self.read_stats_daily_cutoff_date().await {
Ok(value) => value,
Err(err) if dashboard_should_fallback_to_raw_on_aggregate_error(&err) => {
return self
.summarize_dashboard_usage_raw(
query.created_from_unix_secs,
query.created_until_unix_secs,
query.user_id.as_deref(),
)
.await;
}
Err(err) => return Err(err),
};
let Some(cutoff_utc) = cutoff_utc else {
return self
.summarize_dashboard_usage_raw(
query.created_from_unix_secs,
@@ -4099,13 +4144,26 @@ ORDER BY created_at_unix_secs ASC, group_id ASC, usage_id ASC
absorb_dashboard_summary(&mut summary, &raw);
}
if let Some((aggregate_start, aggregate_end)) = split.aggregate {
let aggregate = self
let aggregate = match self
.summarize_dashboard_usage_from_daily_aggregates(
aggregate_start,
aggregate_end,
query.user_id.as_deref(),
)
.await?;
.await
{
Ok(value) => value,
Err(err) if dashboard_should_fallback_to_raw_on_aggregate_error(&err) => {
return self
.summarize_dashboard_usage_raw(
query.created_from_unix_secs,
query.created_until_unix_secs,
query.user_id.as_deref(),
)
.await;
}
Err(err) => return Err(err),
};
absorb_dashboard_summary(&mut summary, &aggregate);
}
if let Some((raw_start, raw_end)) = split.raw_trailing {
@@ -4300,7 +4358,14 @@ ORDER BY date ASC, total_cost_usd DESC, "usage".model ASC, "usage".provider_name
return self.list_dashboard_daily_breakdown_raw(query).await;
}
let Some(cutoff_utc) = self.read_stats_daily_cutoff_date().await? else {
let cutoff_utc = match self.read_stats_daily_cutoff_date().await {
Ok(value) => value,
Err(err) if dashboard_should_fallback_to_raw_on_aggregate_error(&err) => {
return self.list_dashboard_daily_breakdown_raw(query).await;
}
Err(err) => return Err(err),
};
let Some(cutoff_utc) = cutoff_utc else {
return self.list_dashboard_daily_breakdown_raw(query).await;
};
let start_utc = dashboard_unix_secs_to_utc(query.created_from_unix_secs);
@@ -4323,14 +4388,21 @@ ORDER BY date ASC, total_cost_usd DESC, "usage".model ASC, "usage".provider_name
);
}
if let Some((aggregate_start, aggregate_end)) = split.aggregate {
items.extend(
self.list_dashboard_daily_breakdown_from_daily_aggregates(
let aggregate_rows = match self
.list_dashboard_daily_breakdown_from_daily_aggregates(
aggregate_start,
aggregate_end,
query.user_id.as_deref(),
)
.await?,
);
.await
{
Ok(value) => value,
Err(err) if dashboard_should_fallback_to_raw_on_aggregate_error(&err) => {
return self.list_dashboard_daily_breakdown_raw(query).await;
}
Err(err) => return Err(err),
};
items.extend(aggregate_rows);
}
if let Some((raw_start, raw_end)) = split.raw_trailing {
items.extend(

View File

@@ -500,6 +500,34 @@ fn usage_sql_summarize_total_tokens_by_api_key_ids_supports_daily_aggregates() {
assert!(source.contains("read_stats_daily_cutoff_date().await?"));
}
#[test]
fn dashboard_aggregate_schema_mismatch_detector_matches_legacy_schema_failures() {
assert!(super::dashboard_aggregate_schema_mismatch_message(
"postgres error: error occurred while decoding column \"cutoff_date\": \
mismatched types; Rust type `chrono::DateTime<Utc>` (as SQL type `TIMESTAMPTZ`) \
is not compatible with SQL type `INT8`"
));
assert!(super::dashboard_aggregate_schema_mismatch_message(
"postgres error: db error: ERROR: relation \"stats_daily_model_provider\" does not exist"
));
assert!(super::dashboard_aggregate_schema_mismatch_message(
"postgres error: db error: ERROR: column \"effective_input_tokens\" does not exist"
));
assert!(!super::dashboard_aggregate_schema_mismatch_message(
"postgres error: db error: ERROR: permission denied for relation stats_daily"
));
}
#[test]
fn dashboard_aggregate_reads_fallback_to_raw_on_schema_mismatch() {
let source = include_str!("mod.rs");
assert!(source.contains("dashboard_should_fallback_to_raw_on_aggregate_error"));
assert!(
source.contains("Err(err) if dashboard_should_fallback_to_raw_on_aggregate_error(&err)")
);
assert!(source.contains("return self.list_dashboard_daily_breakdown_raw(query).await;"));
}
#[test]
fn usage_sql_summarize_usage_totals_by_user_ids_supports_user_summary_aggregates() {
let source = include_str!("mod.rs");