From 20734330a6fe745ef9e4ab488490dd5a5da72ea1 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 10:16:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugin):=20SQL=20=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=20=E2=80=94=20Generated=20Column=20=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E4=BC=98=E5=85=88=E4=BD=BF=E7=94=A8=20=5Ff=5F=20?= =?UTF-8?q?=E5=89=8D=E7=BC=80=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/erp-plugin/src/dynamic_table.rs | 156 +++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/crates/erp-plugin/src/dynamic_table.rs b/crates/erp-plugin/src/dynamic_table.rs index 2af0eac..99568c2 100644 --- a/crates/erp-plugin/src/dynamic_table.rs +++ b/crates/erp-plugin/src/dynamic_table.rs @@ -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, + search: Option<(String, String)>, + sort_by: Option, + sort_order: Option, + generated_fields: &[String], + ) -> Result<(String, Vec), 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 = 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 = 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 + ); + } }