feat(plugin): SQL 查询路由 — Generated Column 字段优先使用 _f_ 前缀列
This commit is contained in:
@@ -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 Column,got: {}",
|
||||
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 Column,got: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user