1pub use crate::orm::hooks::{
91 GlobalScope, HookContext, HookDispatcher, HookEvent, HookFn, HookRegistry, HookResult,
92 Hookable, ScopeRegistry, SoftDelete, SoftDeleteScope, TenantModel, TenantScope,
93};
94
95pub const ALL_EVENTS: [HookEvent; 16] = [
103 HookEvent::BeforeInsert,
105 HookEvent::AfterInsert,
106 HookEvent::BeforeUpdate,
107 HookEvent::AfterUpdate,
108 HookEvent::BeforeDelete,
109 HookEvent::AfterDelete,
110 HookEvent::BeforeFind,
111 HookEvent::AfterFind,
112 HookEvent::BeforeRestore,
113 HookEvent::AfterRestore,
114 HookEvent::BeforeWrite,
115 HookEvent::AfterWrite,
116 HookEvent::BeforeSave,
118 HookEvent::AfterSave,
119 HookEvent::BeforeValidate,
120 HookEvent::AfterValidate,
121];
122
123pub const PHP_NATIVE_EVENTS: [HookEvent; 12] = [
125 HookEvent::BeforeInsert,
126 HookEvent::AfterInsert,
127 HookEvent::BeforeUpdate,
128 HookEvent::AfterUpdate,
129 HookEvent::BeforeDelete,
130 HookEvent::AfterDelete,
131 HookEvent::BeforeFind,
132 HookEvent::AfterFind,
133 HookEvent::BeforeRestore,
134 HookEvent::AfterRestore,
135 HookEvent::BeforeWrite,
136 HookEvent::AfterWrite,
137];
138
139pub const EXTENDED_EVENTS: [HookEvent; 4] = [
141 HookEvent::BeforeSave,
142 HookEvent::AfterSave,
143 HookEvent::BeforeValidate,
144 HookEvent::AfterValidate,
145];
146
147pub fn event_name(event: HookEvent) -> &'static str {
165 match event {
166 HookEvent::BeforeInsert => "before_insert",
167 HookEvent::AfterInsert => "after_insert",
168 HookEvent::BeforeUpdate => "before_update",
169 HookEvent::AfterUpdate => "after_update",
170 HookEvent::BeforeDelete => "before_delete",
171 HookEvent::AfterDelete => "after_delete",
172 HookEvent::BeforeWrite => "before_write",
173 HookEvent::AfterWrite => "after_write",
174 HookEvent::BeforeSave => "before_save",
175 HookEvent::AfterSave => "after_save",
176 HookEvent::BeforeRestore => "before_restore",
177 HookEvent::AfterRestore => "after_restore",
178 HookEvent::BeforeFind => "before_find",
179 HookEvent::AfterFind => "after_find",
180 HookEvent::BeforeValidate => "before_validate",
181 HookEvent::AfterValidate => "after_validate",
182 }
183}
184
185pub fn event_from_name(name: &str) -> Option<HookEvent> {
199 match name {
200 "before_insert" => Some(HookEvent::BeforeInsert),
201 "after_insert" => Some(HookEvent::AfterInsert),
202 "before_update" => Some(HookEvent::BeforeUpdate),
203 "after_update" => Some(HookEvent::AfterUpdate),
204 "before_delete" => Some(HookEvent::BeforeDelete),
205 "after_delete" => Some(HookEvent::AfterDelete),
206 "before_write" => Some(HookEvent::BeforeWrite),
207 "after_write" => Some(HookEvent::AfterWrite),
208 "before_save" => Some(HookEvent::BeforeSave),
209 "after_save" => Some(HookEvent::AfterSave),
210 "before_restore" => Some(HookEvent::BeforeRestore),
211 "after_restore" => Some(HookEvent::AfterRestore),
212 "before_find" => Some(HookEvent::BeforeFind),
213 "after_find" => Some(HookEvent::AfterFind),
214 "before_validate" => Some(HookEvent::BeforeValidate),
215 "after_validate" => Some(HookEvent::AfterValidate),
216 _ => None,
217 }
218}
219
220pub const INSERT_ORDER: [HookEvent; 8] = [
233 HookEvent::BeforeWrite,
234 HookEvent::BeforeSave,
235 HookEvent::BeforeValidate,
236 HookEvent::AfterValidate,
237 HookEvent::BeforeInsert,
238 HookEvent::AfterInsert,
239 HookEvent::AfterSave,
240 HookEvent::AfterWrite,
241];
242
243pub const UPDATE_ORDER: [HookEvent; 8] = [
245 HookEvent::BeforeWrite,
246 HookEvent::BeforeSave,
247 HookEvent::BeforeValidate,
248 HookEvent::AfterValidate,
249 HookEvent::BeforeUpdate,
250 HookEvent::AfterUpdate,
251 HookEvent::AfterSave,
252 HookEvent::AfterWrite,
253];
254
255pub const DELETE_ORDER: [HookEvent; 2] = [HookEvent::BeforeDelete, HookEvent::AfterDelete];
257
258pub const RESTORE_ORDER: [HookEvent; 2] = [HookEvent::BeforeRestore, HookEvent::AfterRestore];
260
261pub const FIND_ORDER: [HookEvent; 2] = [HookEvent::BeforeFind, HookEvent::AfterFind];
263
264use std::sync::{Arc, Mutex};
269
270pub struct HookExecutionRecorder {
292 events: Mutex<Vec<HookEvent>>,
293}
294
295impl Default for HookExecutionRecorder {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl HookExecutionRecorder {
302 pub fn new() -> Self {
304 Self {
305 events: Mutex::new(Vec::new()),
306 }
307 }
308
309 pub fn record(&self, event: HookEvent) {
311 if let Ok(mut events) = self.events.lock() {
312 events.push(event);
313 }
314 }
315
316 pub fn events(&self) -> Vec<HookEvent> {
318 self.events
319 .lock()
320 .map(|events| events.clone())
321 .unwrap_or_default()
322 }
323
324 pub fn event_names(&self) -> Vec<&'static str> {
326 self.events
327 .lock()
328 .map(|events| events.iter().map(|e| event_name(*e)).collect())
329 .unwrap_or_default()
330 }
331
332 pub fn clear(&self) {
334 if let Ok(mut events) = self.events.lock() {
335 events.clear();
336 }
337 }
338
339 pub fn len(&self) -> usize {
341 self.events.lock().map(|e| e.len()).unwrap_or(0)
342 }
343
344 pub fn is_empty(&self) -> bool {
346 self.len() == 0
347 }
348
349 pub fn assert_order(&self, expected: &[HookEvent]) -> Result<(), String> {
353 let actual = self.events();
354 if actual.len() != expected.len() {
355 return Err(format!(
356 "事件数量不匹配:expected {} 个 {:?},actual {} 个 {:?}",
357 expected.len(),
358 expected.iter().map(|e| event_name(*e)).collect::<Vec<_>>(),
359 actual.len(),
360 actual.iter().map(|e| event_name(*e)).collect::<Vec<_>>(),
361 ));
362 }
363 for (i, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
364 if actual != expected {
365 return Err(format!(
366 "事件顺序不一致 @ index {}:expected {:?},actual {:?}",
367 i,
368 event_name(*expected),
369 event_name(*actual)
370 ));
371 }
372 }
373 Ok(())
374 }
375}
376
377pub fn validate_insert_order(registry: &HookRegistry) -> Result<(), String> {
389 let recorder = Arc::new(HookExecutionRecorder::new());
390 for event in INSERT_ORDER.iter() {
391 let r = Arc::clone(&recorder);
392 registry.register(
393 *event,
394 Arc::new(move |_ctx| {
395 r.record(*event);
396 Ok(())
397 }),
398 );
399 }
400 let ctx = HookContext::new();
401 for event in INSERT_ORDER.iter() {
402 registry
403 .dispatch(*event, &ctx)
404 .map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
405 }
406 recorder.assert_order(&INSERT_ORDER)
407}
408
409pub fn validate_update_order(registry: &HookRegistry) -> Result<(), String> {
411 let recorder = Arc::new(HookExecutionRecorder::new());
412 for event in UPDATE_ORDER.iter() {
413 let r = Arc::clone(&recorder);
414 registry.register(
415 *event,
416 Arc::new(move |_ctx| {
417 r.record(*event);
418 Ok(())
419 }),
420 );
421 }
422 let ctx = HookContext::new();
423 for event in UPDATE_ORDER.iter() {
424 registry
425 .dispatch(*event, &ctx)
426 .map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
427 }
428 recorder.assert_order(&UPDATE_ORDER)
429}
430
431pub fn validate_delete_order(registry: &HookRegistry) -> Result<(), String> {
433 let recorder = Arc::new(HookExecutionRecorder::new());
434 for event in DELETE_ORDER.iter() {
435 let r = Arc::clone(&recorder);
436 registry.register(
437 *event,
438 Arc::new(move |_ctx| {
439 r.record(*event);
440 Ok(())
441 }),
442 );
443 }
444 let ctx = HookContext::new();
445 for event in DELETE_ORDER.iter() {
446 registry
447 .dispatch(*event, &ctx)
448 .map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
449 }
450 recorder.assert_order(&DELETE_ORDER)
451}
452
453pub fn validate_restore_order(registry: &HookRegistry) -> Result<(), String> {
455 let recorder = Arc::new(HookExecutionRecorder::new());
456 for event in RESTORE_ORDER.iter() {
457 let r = Arc::clone(&recorder);
458 registry.register(
459 *event,
460 Arc::new(move |_ctx| {
461 r.record(*event);
462 Ok(())
463 }),
464 );
465 }
466 let ctx = HookContext::new();
467 for event in RESTORE_ORDER.iter() {
468 registry
469 .dispatch(*event, &ctx)
470 .map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
471 }
472 recorder.assert_order(&RESTORE_ORDER)
473}
474
475pub fn validate_find_order(registry: &HookRegistry) -> Result<(), String> {
477 let recorder = Arc::new(HookExecutionRecorder::new());
478 for event in FIND_ORDER.iter() {
479 let r = Arc::clone(&recorder);
480 registry.register(
481 *event,
482 Arc::new(move |_ctx| {
483 r.record(*event);
484 Ok(())
485 }),
486 );
487 }
488 let ctx = HookContext::new();
489 for event in FIND_ORDER.iter() {
490 registry
491 .dispatch(*event, &ctx)
492 .map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
493 }
494 recorder.assert_order(&FIND_ORDER)
495}
496
497pub trait HookContextExt {
559 fn with_meta(self, key: impl Into<String>, value: impl Into<String>) -> Self;
566
567 fn with_metas<I, K, V>(self, entries: I) -> Self
572 where
573 I: IntoIterator<Item = (K, V)>,
574 K: Into<String>,
575 V: Into<String>;
576}
577
578impl HookContextExt for HookContext {
579 fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
580 self.metadata.insert(key.into(), value.into());
581 self
582 }
583
584 fn with_metas<I, K, V>(mut self, entries: I) -> Self
585 where
586 I: IntoIterator<Item = (K, V)>,
587 K: Into<String>,
588 V: Into<String>,
589 {
590 for (key, value) in entries {
591 self.metadata.insert(key.into(), value.into());
592 }
593 self
594 }
595}
596
597pub fn hook_context() -> HookContext {
612 HookContext::new()
613}
614
615pub fn hook_context_with_tenant(tenant_id: i64) -> HookContext {
619 HookContext::new().with_tenant(tenant_id)
620}
621
622pub fn hook_context_with_operator(operator_id: i64) -> HookContext {
626 HookContext::new().with_operator(operator_id)
627}
628
629pub fn hook_context_from_meta<I, K, V>(entries: I) -> HookContext
647where
648 I: IntoIterator<Item = (K, V)>,
649 K: Into<String>,
650 V: Into<String>,
651{
652 HookContext::new().with_metas(entries)
653}
654
655pub const DEFAULT_SOFT_DELETE_FIELD: &str = "delete_time";
692
693pub fn soft_delete_filter_sql(field: &str) -> String {
706 format!("{} IS NULL", field)
707}
708
709pub fn only_trashed_filter_sql(field: &str) -> String {
721 format!("{} IS NOT NULL", field)
722}
723
724pub fn is_soft_deleted(field_value: Option<&str>) -> bool {
756 match field_value {
757 None => false,
758 Some(v) => !v.is_empty(),
759 }
760}
761
762pub fn soft_delete_update_sql(table: &str, field: &str, pk: &str) -> String {
778 format!("UPDATE {} SET {} = NOW() WHERE {} = ?", table, field, pk)
779}
780
781pub fn soft_delete_restore_sql(table: &str, field: &str, pk: &str) -> String {
794 format!("UPDATE {} SET {} = NULL WHERE {} = ?", table, field, pk)
795}
796
797pub const DEFAULT_TENANT_FIELD: &str = "app_id";
842
843pub fn tenant_filter_sql(field: &str, table: &str) -> String {
865 format!("{}.{} = ?", table, field)
866}
867
868pub fn tenant_filter_sql_no_table(field: &str) -> String {
890 format!("{} = ?", field)
891}
892
893pub fn is_tenant_aware(app_id: i64) -> bool {
919 app_id > 0
920}
921
922#[cfg(test)]
927mod tests {
928 use super::*;
929
930 #[test]
935 fn test_all_events_count() {
936 assert_eq!(ALL_EVENTS.len(), 16, "16 事件完整列表");
937 }
938
939 #[test]
940 fn test_php_native_events_count() {
941 assert_eq!(PHP_NATIVE_EVENTS.len(), 12, "PHP 原生 12 事件");
942 }
943
944 #[test]
945 fn test_extended_events_count() {
946 assert_eq!(EXTENDED_EVENTS.len(), 4, "sz-orm-core 扩展 4 事件");
947 }
948
949 #[test]
950 fn test_all_events_no_duplicates() {
951 let mut names: Vec<&str> = ALL_EVENTS.iter().map(|e| event_name(*e)).collect();
953 names.sort();
954 names.dedup();
955 assert_eq!(names.len(), ALL_EVENTS.len(), "ALL_EVENTS 不应有重复");
956 }
957
958 #[test]
959 fn test_native_plus_extended_equals_all() {
960 let mut all_names: Vec<&str> = ALL_EVENTS.iter().map(|e| event_name(*e)).collect();
962 all_names.sort();
963
964 let mut combined: Vec<&str> = PHP_NATIVE_EVENTS
965 .iter()
966 .chain(EXTENDED_EVENTS.iter())
967 .map(|e| event_name(*e))
968 .collect();
969 combined.sort();
970
971 assert_eq!(combined, all_names, "PHP_NATIVE + EXTENDED == ALL");
972 }
973
974 #[test]
979 fn test_event_name_php_style() {
980 assert_eq!(event_name(HookEvent::BeforeInsert), "before_insert");
982 assert_eq!(event_name(HookEvent::AfterInsert), "after_insert");
983 assert_eq!(event_name(HookEvent::BeforeUpdate), "before_update");
984 assert_eq!(event_name(HookEvent::AfterUpdate), "after_update");
985 assert_eq!(event_name(HookEvent::BeforeDelete), "before_delete");
986 assert_eq!(event_name(HookEvent::AfterDelete), "after_delete");
987 assert_eq!(event_name(HookEvent::BeforeWrite), "before_write");
988 assert_eq!(event_name(HookEvent::AfterWrite), "after_write");
989 assert_eq!(event_name(HookEvent::BeforeSave), "before_save");
990 assert_eq!(event_name(HookEvent::AfterSave), "after_save");
991 assert_eq!(event_name(HookEvent::BeforeRestore), "before_restore");
992 assert_eq!(event_name(HookEvent::AfterRestore), "after_restore");
993 assert_eq!(event_name(HookEvent::BeforeFind), "before_find");
994 assert_eq!(event_name(HookEvent::AfterFind), "after_find");
995 assert_eq!(event_name(HookEvent::BeforeValidate), "before_validate");
996 assert_eq!(event_name(HookEvent::AfterValidate), "after_validate");
997 }
998
999 #[test]
1000 fn test_event_from_name_roundtrip() {
1001 for event in ALL_EVENTS.iter() {
1003 let name = event_name(*event);
1004 let back = event_from_name(name);
1005 assert_eq!(back, Some(*event), "事件 {:?} 往返映射失败", event);
1006 }
1007 }
1008
1009 #[test]
1010 fn test_event_from_name_unknown() {
1011 assert_eq!(event_from_name("unknown_event"), None);
1012 assert_eq!(event_from_name(""), None);
1013 assert_eq!(event_from_name("BeforeInsert"), None, "大小写敏感");
1014 assert_eq!(event_from_name("before-insert"), None, "需 snake_case");
1015 }
1016
1017 #[test]
1022 fn test_insert_order_aligns_php() {
1023 assert_eq!(
1029 INSERT_ORDER,
1030 [
1031 HookEvent::BeforeWrite,
1032 HookEvent::BeforeSave,
1033 HookEvent::BeforeValidate,
1034 HookEvent::AfterValidate,
1035 HookEvent::BeforeInsert,
1036 HookEvent::AfterInsert,
1037 HookEvent::AfterSave,
1038 HookEvent::AfterWrite,
1039 ]
1040 );
1041 let php_order = [
1043 HookEvent::BeforeWrite,
1044 HookEvent::BeforeInsert,
1045 HookEvent::AfterInsert,
1046 HookEvent::AfterWrite,
1047 ];
1048 let mut php_idx = 0;
1049 for event in INSERT_ORDER.iter() {
1050 if php_idx < php_order.len() && *event == php_order[php_idx] {
1051 php_idx += 1;
1052 }
1053 }
1054 assert_eq!(php_idx, php_order.len(), "PHP 原生顺序应作为子序列保留");
1055 }
1056
1057 #[test]
1058 fn test_update_order_aligns_php() {
1059 assert_eq!(
1062 UPDATE_ORDER,
1063 [
1064 HookEvent::BeforeWrite,
1065 HookEvent::BeforeSave,
1066 HookEvent::BeforeValidate,
1067 HookEvent::AfterValidate,
1068 HookEvent::BeforeUpdate,
1069 HookEvent::AfterUpdate,
1070 HookEvent::AfterSave,
1071 HookEvent::AfterWrite,
1072 ]
1073 );
1074 }
1075
1076 #[test]
1077 fn test_delete_order_aligns_php() {
1078 assert_eq!(
1080 DELETE_ORDER,
1081 [HookEvent::BeforeDelete, HookEvent::AfterDelete]
1082 );
1083 }
1084
1085 #[test]
1086 fn test_restore_order_aligns_php() {
1087 assert_eq!(
1089 RESTORE_ORDER,
1090 [HookEvent::BeforeRestore, HookEvent::AfterRestore]
1091 );
1092 }
1093
1094 #[test]
1095 fn test_find_order_aligns_php() {
1096 assert_eq!(FIND_ORDER, [HookEvent::BeforeFind, HookEvent::AfterFind]);
1098 }
1099
1100 #[test]
1105 fn test_recorder_empty() {
1106 let r = HookExecutionRecorder::new();
1107 assert!(r.is_empty());
1108 assert_eq!(r.len(), 0);
1109 assert_eq!(r.events(), Vec::<HookEvent>::new());
1110 assert_eq!(r.event_names(), Vec::<&str>::new());
1111 }
1112
1113 #[test]
1114 fn test_recorder_record_and_read() {
1115 let r = HookExecutionRecorder::new();
1116 r.record(HookEvent::BeforeInsert);
1117 r.record(HookEvent::AfterInsert);
1118 assert_eq!(r.len(), 2);
1119 assert_eq!(
1120 r.events(),
1121 vec![HookEvent::BeforeInsert, HookEvent::AfterInsert]
1122 );
1123 assert_eq!(r.event_names(), vec!["before_insert", "after_insert"]);
1124 }
1125
1126 #[test]
1127 fn test_recorder_clear() {
1128 let r = HookExecutionRecorder::new();
1129 r.record(HookEvent::BeforeInsert);
1130 r.clear();
1131 assert!(r.is_empty());
1132 }
1133
1134 #[test]
1135 fn test_recorder_assert_order_ok() {
1136 let r = HookExecutionRecorder::new();
1137 r.record(HookEvent::BeforeInsert);
1138 r.record(HookEvent::AfterInsert);
1139 let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
1140 assert!(result.is_ok());
1141 }
1142
1143 #[test]
1144 fn test_recorder_assert_order_mismatch_count() {
1145 let r = HookExecutionRecorder::new();
1146 r.record(HookEvent::BeforeInsert);
1147 let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
1148 assert!(result.is_err());
1149 assert!(result.unwrap_err().contains("数量不匹配"));
1150 }
1151
1152 #[test]
1153 fn test_recorder_assert_order_mismatch_value() {
1154 let r = HookExecutionRecorder::new();
1155 r.record(HookEvent::BeforeInsert);
1156 r.record(HookEvent::BeforeUpdate);
1157 let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
1158 assert!(result.is_err());
1159 let err = result.unwrap_err();
1160 assert!(err.contains("顺序不一致"));
1161 assert!(err.contains("after_insert"));
1162 }
1163
1164 #[test]
1169 fn test_validate_insert_order() {
1170 let registry = HookRegistry::new();
1171 let result = validate_insert_order(®istry);
1172 assert!(result.is_ok(), "INSERT 顺序验证应通过:{:?}", result);
1173 }
1174
1175 #[test]
1176 fn test_validate_update_order() {
1177 let registry = HookRegistry::new();
1178 let result = validate_update_order(®istry);
1179 assert!(result.is_ok(), "UPDATE 顺序验证应通过:{:?}", result);
1180 }
1181
1182 #[test]
1183 fn test_validate_delete_order() {
1184 let registry = HookRegistry::new();
1185 let result = validate_delete_order(®istry);
1186 assert!(result.is_ok(), "DELETE 顺序验证应通过:{:?}", result);
1187 }
1188
1189 #[test]
1190 fn test_validate_restore_order() {
1191 let registry = HookRegistry::new();
1192 let result = validate_restore_order(®istry);
1193 assert!(result.is_ok(), "RESTORE 顺序验证应通过:{:?}", result);
1194 }
1195
1196 #[test]
1197 fn test_validate_find_order() {
1198 let registry = HookRegistry::new();
1199 let result = validate_find_order(®istry);
1200 assert!(result.is_ok(), "FIND 顺序验证应通过:{:?}", result);
1201 }
1202
1203 #[test]
1208 fn test_all_16_events_registerable_and_dispatchable() {
1209 let registry = HookRegistry::new();
1210 let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
1211
1212 for event in ALL_EVENTS.iter() {
1213 let c = Arc::clone(&counter);
1214 registry.register(
1215 *event,
1216 Arc::new(move |_ctx| {
1217 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1218 Ok(())
1219 }),
1220 );
1221 }
1222
1223 let ctx = HookContext::new();
1224 for event in ALL_EVENTS.iter() {
1225 registry.dispatch(*event, &ctx).expect("dispatch 应成功");
1226 }
1227
1228 assert_eq!(
1229 counter.load(std::sync::atomic::Ordering::SeqCst),
1230 16,
1231 "16 事件全部应被触发一次"
1232 );
1233 }
1234
1235 #[test]
1236 fn test_all_16_events_countable() {
1237 let registry = HookRegistry::new();
1238 for event in ALL_EVENTS.iter() {
1239 registry.register(*event, Arc::new(|_ctx| Ok(())));
1240 }
1241 for event in ALL_EVENTS.iter() {
1242 assert_eq!(
1243 registry.count(*event),
1244 1,
1245 "事件 {:?} 应注册 1 个钩子",
1246 event
1247 );
1248 }
1249 }
1250
1251 #[test]
1257 fn test_r5_php_native_12_events_available() {
1258 for event in PHP_NATIVE_EVENTS.iter() {
1261 let name = event_name(*event);
1262 let back = event_from_name(name);
1263 assert_eq!(
1264 back,
1265 Some(*event),
1266 "PHP 原生事件 {:?} 应在 sz-rust 中可用",
1267 name
1268 );
1269 }
1270 }
1271
1272 #[test]
1276 fn test_r5_php_insert_order_preserved() {
1277 let php_native_order = [
1278 HookEvent::BeforeWrite,
1279 HookEvent::BeforeInsert,
1280 HookEvent::AfterInsert,
1281 HookEvent::AfterWrite,
1282 ];
1283 let mut php_idx = 0;
1284 for event in INSERT_ORDER.iter() {
1285 if php_idx < php_native_order.len() && *event == php_native_order[php_idx] {
1286 php_idx += 1;
1287 }
1288 }
1289 assert_eq!(
1290 php_idx,
1291 php_native_order.len(),
1292 "PHP 原生 INSERT 顺序应作为 sz-rust 扩展顺序的子序列保留"
1293 );
1294 }
1295
1296 #[test]
1298 fn test_r5_php_update_order_preserved() {
1299 let php_native_order = [
1300 HookEvent::BeforeWrite,
1301 HookEvent::BeforeUpdate,
1302 HookEvent::AfterUpdate,
1303 HookEvent::AfterWrite,
1304 ];
1305 let mut php_idx = 0;
1306 for event in UPDATE_ORDER.iter() {
1307 if php_idx < php_native_order.len() && *event == php_native_order[php_idx] {
1308 php_idx += 1;
1309 }
1310 }
1311 assert_eq!(
1312 php_idx,
1313 php_native_order.len(),
1314 "PHP 原生 UPDATE 顺序应作为 sz-rust 扩展顺序的子序列保留"
1315 );
1316 }
1317
1318 #[test]
1320 fn test_r5_php_delete_order_exact() {
1321 assert_eq!(
1322 DELETE_ORDER,
1323 [HookEvent::BeforeDelete, HookEvent::AfterDelete],
1324 "DELETE 顺序应与 PHP 完全一致(无扩展)"
1325 );
1326 }
1327
1328 #[test]
1330 fn test_r5_php_restore_order_exact() {
1331 assert_eq!(
1332 RESTORE_ORDER,
1333 [HookEvent::BeforeRestore, HookEvent::AfterRestore],
1334 "RESTORE 顺序应与 PHP 完全一致(无扩展)"
1335 );
1336 }
1337
1338 #[test]
1340 fn test_r5_php_find_order_exact() {
1341 assert_eq!(
1342 FIND_ORDER,
1343 [HookEvent::BeforeFind, HookEvent::AfterFind],
1344 "FIND 顺序应与 PHP 完全一致(无扩展)"
1345 );
1346 }
1347
1348 #[test]
1353 fn test_r5_php_actual_usage_before_insert_via_registry() {
1354 let registry = HookRegistry::new();
1359 let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1360 let c = Arc::clone(&called);
1361 registry.register(
1362 HookEvent::BeforeInsert,
1363 Arc::new(move |_ctx| {
1364 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1365 Ok(())
1366 }),
1367 );
1368 let ctx = HookContext::new();
1369 registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
1370 assert_eq!(
1371 called.load(std::sync::atomic::Ordering::SeqCst),
1372 1,
1373 "before_insert 钩子应被触发"
1374 );
1375 }
1376
1377 #[test]
1384 fn test_r5_php_business_level_before_insert_convention_documented() {
1385 let registry = HookRegistry::new();
1389 let recorder = Arc::new(HookExecutionRecorder::new());
1390 let r = Arc::clone(&recorder);
1391 registry.register(
1392 HookEvent::BeforeInsert,
1393 Arc::new(move |_ctx| {
1394 r.record(HookEvent::BeforeInsert);
1395 Ok(())
1396 }),
1397 );
1398 let ctx = HookContext::new();
1399 registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
1400 assert_eq!(recorder.events(), vec![HookEvent::BeforeInsert]);
1401 }
1402
1403 #[test]
1408 fn test_hook_context_re_exported() {
1409 let ctx = HookContext::new()
1410 .with_tenant(42)
1411 .with_operator(1)
1412 .with_timestamp(1700000000);
1413 assert_eq!(ctx.tenant_id, Some(42));
1414 assert_eq!(ctx.operator_id, Some(1));
1415 assert_eq!(ctx.timestamp, 1700000000);
1416 }
1417
1418 #[test]
1419 fn test_hook_context_metadata() {
1420 let mut ctx = HookContext::new();
1421 ctx.set_meta("source", "api");
1422 ctx.set_meta("ip", "127.0.0.1");
1423 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1424 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1425 assert_eq!(ctx.get_meta("missing"), None);
1426 }
1427
1428 #[test]
1433 fn test_hook_event_is_before() {
1434 assert!(HookEvent::BeforeInsert.is_before());
1435 assert!(HookEvent::BeforeWrite.is_before());
1436 assert!(HookEvent::BeforeSave.is_before());
1437 assert!(HookEvent::BeforeValidate.is_before());
1438 assert!(HookEvent::BeforeFind.is_before());
1439 assert!(HookEvent::BeforeRestore.is_before());
1440 assert!(!HookEvent::AfterInsert.is_before());
1441 }
1442
1443 #[test]
1444 fn test_hook_event_is_after() {
1445 assert!(HookEvent::AfterInsert.is_after());
1446 assert!(HookEvent::AfterWrite.is_after());
1447 assert!(HookEvent::AfterSave.is_after());
1448 assert!(HookEvent::AfterValidate.is_after());
1449 assert!(HookEvent::AfterFind.is_after());
1450 assert!(HookEvent::AfterRestore.is_after());
1451 assert!(!HookEvent::BeforeInsert.is_after());
1452 }
1453
1454 #[test]
1455 fn test_hook_event_is_write_level() {
1456 assert!(HookEvent::BeforeWrite.is_write_level());
1457 assert!(HookEvent::AfterWrite.is_write_level());
1458 assert!(HookEvent::BeforeSave.is_write_level());
1459 assert!(HookEvent::AfterSave.is_write_level());
1460 assert!(!HookEvent::BeforeInsert.is_write_level());
1461 assert!(!HookEvent::BeforeFind.is_write_level());
1462 }
1463
1464 #[test]
1465 fn test_hook_event_is_find_level() {
1466 assert!(HookEvent::BeforeFind.is_find_level());
1467 assert!(HookEvent::AfterFind.is_find_level());
1468 assert!(!HookEvent::BeforeInsert.is_find_level());
1469 }
1470
1471 #[test]
1472 fn test_hook_event_is_validate_level() {
1473 assert!(HookEvent::BeforeValidate.is_validate_level());
1474 assert!(HookEvent::AfterValidate.is_validate_level());
1475 assert!(!HookEvent::BeforeInsert.is_validate_level());
1476 }
1477
1478 #[test]
1479 fn test_hook_event_is_fine_grained() {
1480 for event in EXTENDED_EVENTS.iter() {
1482 assert!(event.is_fine_grained(), "扩展事件 {:?} 应为细粒度", event);
1483 }
1484 assert!(!HookEvent::BeforeInsert.is_fine_grained());
1486 assert!(!HookEvent::AfterInsert.is_fine_grained());
1487 assert!(!HookEvent::BeforeUpdate.is_fine_grained());
1488 assert!(!HookEvent::AfterUpdate.is_fine_grained());
1489 assert!(!HookEvent::BeforeDelete.is_fine_grained());
1490 assert!(!HookEvent::AfterDelete.is_fine_grained());
1491 }
1492
1493 #[test]
1498 fn test_hook_registry_short_circuit_on_error() {
1499 use crate::orm::DbError;
1501 let registry = HookRegistry::new();
1502 let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1503
1504 let c1 = Arc::clone(&called);
1505 registry.register(
1506 HookEvent::BeforeInsert,
1507 Arc::new(move |_ctx| {
1508 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1509 Ok(())
1510 }),
1511 );
1512
1513 registry.register(
1514 HookEvent::BeforeInsert,
1515 Arc::new(|_ctx| Err(DbError::Hook("second hook failed".into()))),
1516 );
1517
1518 let c3 = Arc::clone(&called);
1519 registry.register(
1520 HookEvent::BeforeInsert,
1521 Arc::new(move |_ctx| {
1522 c3.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1523 Ok(())
1524 }),
1525 );
1526
1527 let ctx = HookContext::new();
1528 let result = registry.dispatch(HookEvent::BeforeInsert, &ctx);
1529 assert!(result.is_err());
1530 assert_eq!(
1531 called.load(std::sync::atomic::Ordering::SeqCst),
1532 1,
1533 "第二个钩子失败后第三个不应执行"
1534 );
1535 }
1536
1537 #[test]
1542 fn test_hook_registry_clear() {
1543 let registry = HookRegistry::new();
1544 registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
1545 assert_eq!(registry.count(HookEvent::BeforeInsert), 1);
1546 registry.clear(HookEvent::BeforeInsert);
1547 assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
1548 }
1549
1550 #[test]
1551 fn test_hook_registry_clear_all() {
1552 let registry = HookRegistry::new();
1553 registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
1554 registry.register(HookEvent::AfterInsert, Arc::new(|_ctx| Ok(())));
1555 registry.register(HookEvent::BeforeUpdate, Arc::new(|_ctx| Ok(())));
1556 registry.clear_all();
1557 assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
1558 assert_eq!(registry.count(HookEvent::AfterInsert), 0);
1559 assert_eq!(registry.count(HookEvent::BeforeUpdate), 0);
1560 }
1561
1562 #[test]
1563 fn test_hook_registry_dispatch_no_hooks() {
1564 let registry = HookRegistry::new();
1565 let ctx = HookContext::new();
1566 assert!(registry.dispatch(HookEvent::BeforeInsert, &ctx).is_ok());
1568 }
1569
1570 #[test]
1575 fn test_scope_registry_enable_disable() {
1576 let registry = ScopeRegistry::new();
1577 assert!(registry.is_enabled("soft_delete"));
1578 assert!(registry.is_enabled("tenant"));
1579
1580 registry.disable("soft_delete");
1581 assert!(!registry.is_enabled("soft_delete"));
1582 assert!(registry.is_enabled("tenant"));
1583
1584 registry.enable("soft_delete");
1585 assert!(registry.is_enabled("soft_delete"));
1586 }
1587
1588 #[test]
1589 fn test_scope_registry_without_scope() {
1590 let registry = ScopeRegistry::new();
1591 assert!(registry.is_enabled("soft_delete"));
1592
1593 let result = registry.without_scope("soft_delete", || {
1594 assert!(!registry.is_enabled("soft_delete"));
1595 42
1596 });
1597
1598 assert_eq!(result, 42);
1599 assert!(registry.is_enabled("soft_delete"));
1600 }
1601
1602 #[test]
1607 fn test_hook_context_builder_tenant_id() {
1608 let ctx = hook_context().with_tenant(42);
1610 assert_eq!(ctx.tenant_id, Some(42));
1611 assert_eq!(ctx.operator_id, None);
1612 assert_eq!(ctx.timestamp, 0);
1613 }
1614
1615 #[test]
1616 fn test_hook_context_builder_operator_id() {
1617 let ctx = hook_context().with_operator(1);
1619 assert_eq!(ctx.tenant_id, None);
1620 assert_eq!(ctx.operator_id, Some(1));
1621 assert_eq!(ctx.timestamp, 0);
1622 }
1623
1624 #[test]
1625 fn test_hook_context_builder_timestamp() {
1626 let ctx = hook_context().with_timestamp(1700000000);
1628 assert_eq!(ctx.tenant_id, None);
1629 assert_eq!(ctx.operator_id, None);
1630 assert_eq!(ctx.timestamp, 1700000000);
1631 }
1632
1633 #[test]
1634 fn test_hook_context_builder_metadata_with_meta() {
1635 let ctx = hook_context()
1637 .with_meta("source", "api")
1638 .with_meta("ip", "127.0.0.1")
1639 .with_meta("trace_id", "abc123");
1640 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1641 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1642 assert_eq!(ctx.get_meta("trace_id"), Some(&"abc123".to_string()));
1643 assert_eq!(ctx.get_meta("missing"), None);
1644 assert_eq!(ctx.metadata.len(), 3);
1645 }
1646
1647 #[test]
1648 fn test_hook_context_builder_metadata_with_metas_vec() {
1649 let entries = vec![
1651 ("source".to_string(), "api".to_string()),
1652 ("ip".to_string(), "127.0.0.1".to_string()),
1653 ("trace_id".to_string(), "abc123".to_string()),
1654 ];
1655 let ctx = hook_context().with_metas(entries);
1656 assert_eq!(ctx.metadata.len(), 3);
1657 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1658 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1659 assert_eq!(ctx.get_meta("trace_id"), Some(&"abc123".to_string()));
1660 }
1661
1662 #[test]
1663 fn test_hook_context_builder_metadata_with_metas_array() {
1664 let ctx = hook_context().with_metas([("source", "api"), ("ip", "127.0.0.1")]);
1666 assert_eq!(ctx.metadata.len(), 2);
1667 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1668 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1669 }
1670
1671 #[test]
1672 fn test_hook_context_builder_full_chain() {
1673 let ctx = hook_context()
1675 .with_tenant(42)
1676 .with_operator(1)
1677 .with_timestamp(1700000000)
1678 .with_meta("source", "api")
1679 .with_meta("ip", "127.0.0.1");
1680 assert_eq!(ctx.tenant_id, Some(42));
1681 assert_eq!(ctx.operator_id, Some(1));
1682 assert_eq!(ctx.timestamp, 1700000000);
1683 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1684 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1685 assert_eq!(ctx.metadata.len(), 2);
1686 }
1687
1688 #[test]
1689 fn test_hook_context_with_meta_overwrite() {
1690 let ctx = hook_context()
1692 .with_meta("source", "api")
1693 .with_meta("source", "web"); assert_eq!(ctx.get_meta("source"), Some(&"web".to_string()));
1695 assert_eq!(ctx.metadata.len(), 1);
1696 }
1697
1698 #[test]
1699 fn test_hook_context_with_metas_empty() {
1700 let ctx = hook_context().with_metas(Vec::<(String, String)>::new());
1702 assert_eq!(ctx.metadata.len(), 0);
1703 assert!(ctx.metadata.is_empty());
1704 }
1705
1706 #[test]
1707 fn test_hook_context_set_meta_vs_with_meta() {
1708 let mut ctx1 = HookContext::new();
1710 ctx1.set_meta("key", "value1");
1711 ctx1.set_meta("key2", "value2");
1712
1713 let ctx2 = hook_context()
1714 .with_meta("key", "value1")
1715 .with_meta("key2", "value2");
1716
1717 assert_eq!(ctx1.metadata, ctx2.metadata);
1718 }
1719
1720 #[test]
1725 fn test_hook_context_convenience_empty() {
1726 let ctx1 = hook_context();
1728 let ctx2 = HookContext::new();
1729 assert_eq!(ctx1.tenant_id, ctx2.tenant_id);
1730 assert_eq!(ctx1.operator_id, ctx2.operator_id);
1731 assert_eq!(ctx1.timestamp, ctx2.timestamp);
1732 assert_eq!(ctx1.metadata, ctx2.metadata);
1733 }
1734
1735 #[test]
1736 fn test_hook_context_convenience_with_tenant() {
1737 let ctx1 = hook_context_with_tenant(42);
1739 let ctx2 = hook_context().with_tenant(42);
1740 assert_eq!(ctx1.tenant_id, Some(42));
1741 assert_eq!(ctx1.tenant_id, ctx2.tenant_id);
1742 }
1743
1744 #[test]
1745 fn test_hook_context_convenience_with_operator() {
1746 let ctx1 = hook_context_with_operator(1);
1748 let ctx2 = hook_context().with_operator(1);
1749 assert_eq!(ctx1.operator_id, Some(1));
1750 assert_eq!(ctx1.operator_id, ctx2.operator_id);
1751 }
1752
1753 #[test]
1754 fn test_hook_context_convenience_from_meta_vec() {
1755 let headers = vec![
1757 ("x-trace-id".to_string(), "abc123".to_string()),
1758 ("x-source".to_string(), "api".to_string()),
1759 ];
1760 let ctx = hook_context_from_meta(headers);
1761 assert_eq!(ctx.get_meta("x-trace-id"), Some(&"abc123".to_string()));
1762 assert_eq!(ctx.get_meta("x-source"), Some(&"api".to_string()));
1763 assert_eq!(ctx.metadata.len(), 2);
1764 }
1765
1766 #[test]
1767 fn test_hook_context_convenience_from_meta_array() {
1768 let ctx = hook_context_from_meta([("k1", "v1"), ("k2", "v2")]);
1770 assert_eq!(ctx.get_meta("k1"), Some(&"v1".to_string()));
1771 assert_eq!(ctx.get_meta("k2"), Some(&"v2".to_string()));
1772 }
1773
1774 #[test]
1775 fn test_hook_context_convenience_from_meta_empty() {
1776 let ctx = hook_context_from_meta(Vec::<(String, String)>::new());
1778 assert!(ctx.metadata.is_empty());
1779 }
1780
1781 #[test]
1791 fn test_r5_php_trigger_context_passing() {
1792 let ctx = hook_context()
1795 .with_operator(1)
1796 .with_timestamp(1700000000)
1797 .with_meta("action", "insert")
1798 .with_meta("model_class", "Worklogs");
1799
1800 assert_eq!(ctx.operator_id, Some(1), "操作人 ID 应可设置");
1802 assert_eq!(ctx.timestamp, 1700000000, "时间戳应可设置");
1803 assert_eq!(
1804 ctx.get_meta("action"),
1805 Some(&"insert".to_string()),
1806 "action 元数据应可设置"
1807 );
1808 assert_eq!(
1809 ctx.get_meta("model_class"),
1810 Some(&"Worklogs".to_string()),
1811 "model_class 元数据应可设置"
1812 );
1813 }
1814
1815 #[test]
1820 fn test_r5_php_auto_fill_timestamp_via_context() {
1821 let now = 1700000000_u64;
1822 let ctx = hook_context().with_timestamp(now);
1823
1824 let create_time = ctx.timestamp;
1826 let update_time = ctx.timestamp;
1827
1828 assert_eq!(create_time, now, "create_time 应从 ctx.timestamp 获取");
1829 assert_eq!(update_time, now, "update_time 应从 ctx.timestamp 获取");
1830 }
1831
1832 #[test]
1836 fn test_r5_php_worklogs_stat_day_via_context_meta() {
1837 let ctx = hook_context()
1838 .with_timestamp(1700000000)
1839 .with_meta("stat_day", "20231114");
1840
1841 assert_eq!(ctx.get_meta("stat_day"), Some(&"20231114".to_string()));
1842 }
1843
1844 #[test]
1848 fn test_r5_php_tenant_context_via_hook_context() {
1849 let ctx = hook_context_with_tenant(42);
1850
1851 assert_eq!(ctx.tenant_id, Some(42), "租户 ID 应可设置");
1852 }
1853
1854 #[test]
1858 fn test_r5_php_operator_context_via_hook_context() {
1859 let ctx = hook_context_with_operator(1);
1860
1861 assert_eq!(ctx.operator_id, Some(1), "操作人 ID 应可设置");
1862 }
1863
1864 #[test]
1869 fn test_r5_php_event_listen_context_via_hook_registry() {
1870 let registry = HookRegistry::new();
1871 let captured_operator_id = Arc::new(std::sync::Mutex::new(None::<i64>));
1872
1873 let c = Arc::clone(&captured_operator_id);
1874 registry.register(
1875 HookEvent::BeforeInsert,
1876 Arc::new(move |ctx| {
1877 *c.lock().unwrap() = ctx.operator_id;
1878 Ok(())
1879 }),
1880 );
1881
1882 let ctx = hook_context_with_operator(42);
1883 registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
1884
1885 assert_eq!(
1886 *captured_operator_id.lock().unwrap(),
1887 Some(42),
1888 "运行时钩子应能读取 ctx.operator_id"
1889 );
1890 }
1891
1892 #[test]
1896 fn test_r5_php_audit_log_metadata_via_hook_context() {
1897 let ctx = hook_context()
1898 .with_operator(1)
1899 .with_meta("ip", "192.168.1.100")
1900 .with_meta("ua", "Mozilla/5.0")
1901 .with_meta("source", "web")
1902 .with_meta("trace_id", "abc-123-def");
1903
1904 assert_eq!(ctx.get_meta("ip"), Some(&"192.168.1.100".to_string()));
1906 assert_eq!(ctx.get_meta("ua"), Some(&"Mozilla/5.0".to_string()));
1907 assert_eq!(ctx.get_meta("source"), Some(&"web".to_string()));
1908 assert_eq!(ctx.get_meta("trace_id"), Some(&"abc-123-def".to_string()));
1909 assert_eq!(ctx.metadata.len(), 4);
1910 }
1911
1912 #[test]
1916 fn test_r5_php_batch_headers_via_hook_context_from_meta() {
1917 let headers = vec![
1918 ("x-request-id".to_string(), "req-001".to_string()),
1919 ("x-trace-id".to_string(), "trace-001".to_string()),
1920 ("x-tenant-id".to_string(), "42".to_string()),
1921 ("x-operator-id".to_string(), "1".to_string()),
1922 ];
1923 let ctx = hook_context_from_meta(headers);
1924
1925 assert_eq!(ctx.get_meta("x-request-id"), Some(&"req-001".to_string()));
1926 assert_eq!(ctx.get_meta("x-trace-id"), Some(&"trace-001".to_string()));
1927 assert_eq!(ctx.get_meta("x-tenant-id"), Some(&"42".to_string()));
1928 assert_eq!(ctx.get_meta("x-operator-id"), Some(&"1".to_string()));
1929 assert_eq!(ctx.metadata.len(), 4);
1930 }
1931
1932 #[test]
1937 fn test_default_soft_delete_field() {
1938 assert_eq!(DEFAULT_SOFT_DELETE_FIELD, "delete_time");
1940 }
1941
1942 #[test]
1943 fn test_soft_delete_filter_sql_default_field() {
1944 let sql = soft_delete_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
1946 assert_eq!(sql, "delete_time IS NULL");
1947 }
1948
1949 #[test]
1950 fn test_soft_delete_filter_sql_custom_field() {
1951 let sql = soft_delete_filter_sql("deleted_at");
1953 assert_eq!(sql, "deleted_at IS NULL");
1954 }
1955
1956 #[test]
1957 fn test_only_trashed_filter_sql_default_field() {
1958 let sql = only_trashed_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
1960 assert_eq!(sql, "delete_time IS NOT NULL");
1961 }
1962
1963 #[test]
1964 fn test_only_trashed_filter_sql_custom_field() {
1965 let sql = only_trashed_filter_sql("deleted_at");
1966 assert_eq!(sql, "deleted_at IS NOT NULL");
1967 }
1968
1969 #[test]
1970 fn test_is_soft_deleted_null() {
1971 assert!(!is_soft_deleted(None));
1973 }
1974
1975 #[test]
1976 fn test_is_soft_deleted_empty_string() {
1977 assert!(!is_soft_deleted(Some("")));
1979 }
1980
1981 #[test]
1982 fn test_is_soft_deleted_datetime_value() {
1983 assert!(is_soft_deleted(Some("2026-07-21 10:00:00")));
1985 }
1986
1987 #[test]
1988 fn test_is_soft_deleted_timestamp_value() {
1989 assert!(is_soft_deleted(Some("1700000000")));
1991 }
1992
1993 #[test]
1994 fn test_is_soft_deleted_zero_string() {
1995 assert!(is_soft_deleted(Some("0")));
2000 }
2001
2002 #[test]
2003 fn test_soft_delete_update_sql_default() {
2004 let sql = soft_delete_update_sql("users", DEFAULT_SOFT_DELETE_FIELD, "id");
2006 assert_eq!(sql, "UPDATE users SET delete_time = NOW() WHERE id = ?");
2007 }
2008
2009 #[test]
2010 fn test_soft_delete_update_sql_custom() {
2011 let sql = soft_delete_update_sql("orders", "deleted_at", "order_id");
2013 assert_eq!(
2014 sql,
2015 "UPDATE orders SET deleted_at = NOW() WHERE order_id = ?"
2016 );
2017 }
2018
2019 #[test]
2020 fn test_soft_delete_restore_sql_default() {
2021 let sql = soft_delete_restore_sql("users", DEFAULT_SOFT_DELETE_FIELD, "id");
2023 assert_eq!(sql, "UPDATE users SET delete_time = NULL WHERE id = ?");
2024 }
2025
2026 #[test]
2027 fn test_soft_delete_restore_sql_custom() {
2028 let sql = soft_delete_restore_sql("orders", "deleted_at", "order_id");
2029 assert_eq!(
2030 sql,
2031 "UPDATE orders SET deleted_at = NULL WHERE order_id = ?"
2032 );
2033 }
2034
2035 #[test]
2041 fn test_r5_php_soft_delete_default_field_name() {
2042 assert_eq!(DEFAULT_SOFT_DELETE_FIELD, "delete_time");
2047 assert_ne!(DEFAULT_SOFT_DELETE_FIELD, "deleted_at");
2048 }
2049
2050 #[test]
2052 fn test_r5_php_with_no_trashed_default_condition() {
2053 let sz_rust_sql = soft_delete_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
2057 assert_eq!(sz_rust_sql, "delete_time IS NULL");
2058 let _ = std::marker::PhantomData::<SoftDeleteScope>;
2060 }
2061
2062 #[test]
2064 fn test_r5_php_trashed_logic() {
2065 assert!(!is_soft_deleted(None), "NULL → 未软删除");
2068 assert!(!is_soft_deleted(Some("")), "空字符串 → 未软删除");
2069 assert!(
2070 is_soft_deleted(Some("2026-07-21 10:00:00")),
2071 "有值 → 已软删除"
2072 );
2073 }
2074
2075 #[test]
2077 fn test_r5_php_only_trashed_condition() {
2078 let sql = only_trashed_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
2079 assert_eq!(sql, "delete_time IS NOT NULL");
2080 }
2081
2082 #[test]
2084 fn test_r5_php_delete_sql_format() {
2085 let sql = soft_delete_update_sql("users", "delete_time", "id");
2087 assert!(sql.contains("UPDATE users"));
2088 assert!(sql.contains("SET delete_time = NOW()"));
2089 assert!(sql.contains("WHERE id = ?"));
2090 }
2091
2092 #[test]
2094 fn test_r5_php_restore_sql_format() {
2095 let sql = soft_delete_restore_sql("users", "delete_time", "id");
2097 assert!(sql.contains("UPDATE users"));
2098 assert!(sql.contains("SET delete_time = NULL"));
2099 assert!(sql.contains("WHERE id = ?"));
2100 }
2101
2102 #[test]
2104 fn test_r5_php_basemodel_no_soft_delete_trait() {
2105 let _ = std::marker::PhantomData::<SoftDeleteScope>;
2111 }
2112
2113 #[test]
2115 fn test_r5_php_upload_file_disable_soft_delete() {
2116 struct UploadFile; let _ = std::marker::PhantomData::<UploadFile>;
2122 }
2124
2125 #[test]
2130 fn test_default_tenant_field() {
2131 assert_eq!(DEFAULT_TENANT_FIELD, "app_id");
2133 assert_ne!(DEFAULT_TENANT_FIELD, "tenant_id");
2134 }
2135
2136 #[test]
2137 fn test_tenant_filter_sql_default_field() {
2138 let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "users");
2140 assert_eq!(sql, "users.app_id = ?");
2141 }
2142
2143 #[test]
2144 fn test_tenant_filter_sql_custom_field() {
2145 let sql = tenant_filter_sql("tenant_id", "orders");
2147 assert_eq!(sql, "orders.tenant_id = ?");
2148 }
2149
2150 #[test]
2151 fn test_tenant_filter_sql_with_alias() {
2152 let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "u");
2154 assert_eq!(sql, "u.app_id = ?");
2155 }
2156
2157 #[test]
2158 fn test_tenant_filter_sql_no_table_default() {
2159 let sql = tenant_filter_sql_no_table(DEFAULT_TENANT_FIELD);
2161 assert_eq!(sql, "app_id = ?");
2162 }
2163
2164 #[test]
2165 fn test_tenant_filter_sql_no_table_custom() {
2166 let sql = tenant_filter_sql_no_table("tenant_id");
2168 assert_eq!(sql, "tenant_id = ?");
2169 }
2170
2171 #[test]
2172 fn test_is_tenant_aware_zero() {
2173 assert!(!is_tenant_aware(0));
2175 }
2176
2177 #[test]
2178 fn test_is_tenant_aware_negative() {
2179 assert!(!is_tenant_aware(-1));
2181 assert!(!is_tenant_aware(-100));
2182 }
2183
2184 #[test]
2185 fn test_is_tenant_aware_positive() {
2186 assert!(is_tenant_aware(1));
2188 assert!(is_tenant_aware(42));
2189 assert!(is_tenant_aware(10000));
2190 }
2191
2192 #[test]
2193 fn test_is_tenant_aware_max_i64() {
2194 assert!(is_tenant_aware(i64::MAX));
2196 }
2197
2198 #[test]
2204 fn test_r5_php_tenant_field_name_app_id() {
2205 assert_eq!(DEFAULT_TENANT_FIELD, "app_id");
2212 assert_ne!(DEFAULT_TENANT_FIELD, "tenant_id");
2213 }
2214
2215 #[test]
2217 fn test_r5_php_scope_app_id_condition() {
2218 let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "sz_user");
2222 assert_eq!(sql, "sz_user.app_id = ?");
2223 assert!(sql.contains(".app_id"));
2225 }
2226
2227 #[test]
2229 fn test_r5_php_app_id_gt_zero_check() {
2230 assert!(!is_tenant_aware(0), "app_id=0 不启用多租户过滤");
2233 assert!(!is_tenant_aware(-1), "app_id=-1 不启用多租户过滤");
2234 assert!(is_tenant_aware(1), "app_id=1 启用多租户过滤");
2235 assert!(is_tenant_aware(42), "app_id=42 启用多租户过滤");
2236 }
2237
2238 #[test]
2240 fn test_r5_php_global_scope_declaration() {
2241 let scope_name = "app_id"; let registry = ScopeRegistry::new();
2246 registry.enable(scope_name);
2248 assert!(registry.is_enabled(scope_name), "app_id 全局作用域已启用");
2249 registry.disable(scope_name);
2251 assert!(!registry.is_enabled(scope_name), "app_id 全局作用域已禁用");
2252 }
2253
2254 #[test]
2256 fn test_r5_php_module_based_app_id_binding() {
2257 let ctx = hook_context_with_tenant(42);
2262 assert_eq!(ctx.tenant_id, Some(42));
2263 let app_id = ctx.tenant_id.unwrap_or(0);
2265 assert!(is_tenant_aware(app_id));
2266 }
2267
2268 #[test]
2270 fn test_r5_php_tenant_scope_vs_sz_orm_core() {
2271 let php_style = tenant_filter_sql_no_table(DEFAULT_TENANT_FIELD);
2275 assert_eq!(php_style, "app_id = ?");
2276 let sz_orm_style = tenant_filter_sql_no_table("tenant_id");
2277 assert_eq!(sz_orm_style, "tenant_id = ?");
2278 assert_ne!(php_style, sz_orm_style);
2280 }
2281
2282 #[test]
2284 fn test_r5_php_tenant_context_passing() {
2285 let ctx1 = hook_context_with_tenant(100);
2288 let ctx2 = hook_context_with_tenant(200);
2289 assert_ne!(ctx1.tenant_id, ctx2.tenant_id);
2291 assert_eq!(ctx1.tenant_id, Some(100));
2292 assert_eq!(ctx2.tenant_id, Some(200));
2293 }
2294
2295 #[test]
2297 fn test_r5_php_no_cross_tenant_query_when_app_id_zero() {
2298 let app_id = 0;
2302 assert!(!is_tenant_aware(app_id));
2303 }
2306}