feat(plugin): timeseries 聚合 API — date_trunc 时间序列

This commit is contained in:
iven
2026-04-17 11:01:43 +08:00
parent c9a58e9d34
commit a333b3673f
5 changed files with 233 additions and 1 deletions

View File

@@ -783,6 +783,61 @@ impl DynamicTableManager {
Ok((sql, values))
}
/// 构建时间序列查询 SQL
pub fn build_timeseries_sql(
table_name: &str,
tenant_id: Uuid,
time_field: &str,
time_grain: &str,
start: Option<&str>,
end: Option<&str>,
) -> Result<(String, Vec<Value>), String> {
let clean_field = sanitize_identifier(time_field);
let grain = match time_grain {
"day" | "week" | "month" => time_grain,
_ => return Err(format!("不支持的 time_grain: {}", time_grain)),
};
let mut conditions = vec![
"\"tenant_id\" = $1".to_string(),
"\"deleted_at\" IS NULL".to_string(),
];
let mut values: Vec<Value> = vec![tenant_id.into()];
let mut param_idx = 2;
if let Some(s) = start {
conditions.push(format!(
"(data->>'{}')::timestamp >= ${}",
clean_field, param_idx
));
values.push(Value::String(Some(Box::new(s.to_string()))));
param_idx += 1;
}
if let Some(e) = end {
conditions.push(format!(
"(data->>'{}')::timestamp < ${}",
clean_field, param_idx
));
values.push(Value::String(Some(Box::new(e.to_string()))));
}
let sql = format!(
"SELECT to_char(date_trunc('{}', (data->>'{}')::timestamp), 'YYYY-MM-DD') as period, \
COUNT(*) as count \
FROM \"{}\" WHERE {} \
GROUP BY date_trunc('{}', (data->>'{}')::timestamp) \
ORDER BY period",
grain,
clean_field,
table_name,
conditions.join(" AND "),
grain,
clean_field,
);
Ok((sql, values))
}
}
#[cfg(test)]
@@ -1375,4 +1430,67 @@ mod tests {
sql
);
}
// ===== build_timeseries_sql 测试 =====
#[test]
fn test_build_timeseries_sql_day_grain() {
let (sql, values) = DynamicTableManager::build_timeseries_sql(
"plugin_test",
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
"occurred_at",
"day",
None,
None,
)
.unwrap();
assert!(sql.contains("date_trunc('day'"), "应有 day 粒度");
assert!(sql.contains("GROUP BY"), "应有 GROUP BY");
assert!(sql.contains("ORDER BY period"), "应按 period 排序");
assert_eq!(values.len(), 1, "仅 tenant_id");
}
#[test]
fn test_build_timeseries_sql_month_grain() {
let (sql, _) = DynamicTableManager::build_timeseries_sql(
"plugin_test",
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
"created_date",
"month",
None,
None,
)
.unwrap();
assert!(sql.contains("date_trunc('month'"), "应有 month 粒度");
}
#[test]
fn test_build_timeseries_sql_with_date_range() {
let (sql, values) = DynamicTableManager::build_timeseries_sql(
"plugin_test",
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
"occurred_at",
"week",
Some("2026-01-01"),
Some("2026-04-01"),
)
.unwrap();
assert!(sql.contains("date_trunc('week'"), "应有 week 粒度");
assert!(sql.contains(">="), "应有 start 条件");
assert!(sql.contains("<"), "应有 end 条件");
assert_eq!(values.len(), 3, "tenant_id + start + end");
}
#[test]
fn test_build_timeseries_sql_invalid_grain() {
let result = DynamicTableManager::build_timeseries_sql(
"plugin_test",
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
"occurred_at",
"hour",
None,
None,
);
assert!(result.is_err(), "不支持的 grain 应报错");
}
}