feat(plugin): SQL 查询路由 — Generated Column 字段优先使用 _f_ 前缀列

This commit is contained in:
iven
2026-04-17 10:16:35 +08:00
parent a897cd7a87
commit 20734330a6

View File

@@ -501,6 +501,102 @@ impl DynamicTableManager {
Ok((sql, values))
}
/// 返回字段引用函数 — Generated Column 存在时用 _f_{name}
pub fn field_reference_fn(generated_fields: &[String]) -> impl Fn(&str) -> String + '_ {
move |field_name: &str| {
let clean = sanitize_identifier(field_name);
if generated_fields.contains(&clean) {
format!("\"_f_{}\"", clean)
} else {
format!("\"data\"->>'{}'", clean)
}
}
}
/// 扩展版查询构建 — 支持 Generated Column 路由
pub fn build_filtered_query_sql_ex(
table_name: &str,
tenant_id: Uuid,
limit: u64,
offset: u64,
filter: Option<serde_json::Value>,
search: Option<(String, String)>,
sort_by: Option<String>,
sort_order: Option<String>,
generated_fields: &[String],
) -> Result<(String, Vec<Value>), String> {
let ref_fn = Self::field_reference_fn(generated_fields);
let mut conditions = vec![
format!("\"tenant_id\" = ${}", 1),
"\"deleted_at\" IS NULL".to_string(),
];
let mut param_idx = 2;
let mut values: Vec<Value> = vec![tenant_id.into()];
// filter
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("{} = ${}", ref_fn(&clean_key), param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
}
// search
if let Some((fields_csv, keyword)) = search {
let escaped = keyword.replace('%', "\\%").replace('_', "\\_");
let fields: Vec<&str> = fields_csv.split(',').collect();
let search_param_idx = param_idx;
let search_conditions: Vec<String> = fields
.iter()
.map(|f| {
let clean = sanitize_identifier(f.trim());
format!("\"data\"->>'{}' ILIKE ${}", clean, search_param_idx)
})
.collect();
conditions.push(format!("({})", search_conditions.join(" OR ")));
values.push(Value::String(Some(Box::new(format!("%{}%", escaped)))));
param_idx += 1;
}
// sort
let order_clause = if let Some(sb) = sort_by {
let clean = sanitize_identifier(&sb);
if clean.is_empty() {
return Err(format!("无效的排序字段名: {}", sb));
}
let dir = match sort_order.as_deref() {
Some("asc") | Some("ASC") => "ASC",
_ => "DESC",
};
format!("ORDER BY {} {}", ref_fn(&clean), dir)
} else {
"ORDER BY \"created_at\" DESC".to_string()
};
let sql = format!(
"SELECT id, data, created_at, updated_at, version FROM \"{}\" WHERE {} {} LIMIT ${} OFFSET ${}",
table_name,
conditions.join(" AND "),
order_clause,
param_idx,
param_idx + 1,
);
values.push((limit as i64).into());
values.push((offset as i64).into());
Ok((sql, values))
}
}
#[cfg(test)]
@@ -822,4 +918,64 @@ mod tests {
"searchable 字段应使用 pg_trgm GIN 索引"
);
}
// ===== field_reference_fn + build_filtered_query_sql_ex 测试 =====
#[test]
fn test_field_reference_uses_generated_column() {
let generated_fields = vec![
"code".to_string(),
"status".to_string(),
"level".to_string(),
];
let ref_fn = DynamicTableManager::field_reference_fn(&generated_fields);
assert_eq!(ref_fn("code"), "\"_f_code\"");
assert_eq!(ref_fn("status"), "\"_f_status\"");
assert_eq!(ref_fn("name"), "\"data\"->>'name'");
assert_eq!(ref_fn("remark"), "\"data\"->>'remark'");
}
#[test]
fn test_filtered_query_uses_generated_column_for_sort() {
let generated_fields = vec!["code".to_string(), "level".to_string()];
let (sql, _) = DynamicTableManager::build_filtered_query_sql_ex(
"plugin_test",
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
20,
0,
None,
None,
Some("level".to_string()),
Some("asc".to_string()),
&generated_fields,
)
.unwrap();
assert!(
sql.contains("ORDER BY \"_f_level\" ASC"),
"排序应使用 Generated Columngot: {}",
sql
);
}
#[test]
fn test_filtered_query_uses_generated_column_for_filter() {
let generated_fields = vec!["status".to_string()];
let (sql, _) = DynamicTableManager::build_filtered_query_sql_ex(
"plugin_test",
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
20,
0,
Some(serde_json::json!({"status": "active"})),
None,
None,
None,
&generated_fields,
)
.unwrap();
assert!(
sql.contains("\"_f_status\" = $"),
"过滤应使用 Generated Columngot: {}",
sql
);
}
}