1pub use sz_rust_orm_facade::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
762const fn is_valid_identifier(name: &str) -> bool {
768 if name.is_empty() || name.len() > 63 {
769 return false;
770 }
771 let bytes = name.as_bytes();
772 if !bytes[0].is_ascii_alphabetic() && bytes[0] != b'_' {
773 return false;
774 }
775 let mut i = 1;
776 while i < bytes.len() {
777 if !bytes[i].is_ascii_alphanumeric() && bytes[i] != b'_' {
778 return false;
779 }
780 i += 1;
781 }
782 true
783}
784
785pub fn soft_delete_update_sql(table: &str, field: &str, pk: &str) -> String {
805 debug_assert!(
806 is_valid_identifier(table),
807 "table must be a valid SQL identifier, got {:?}",
808 table
809 );
810 debug_assert!(
811 is_valid_identifier(field),
812 "field must be a valid SQL identifier, got {:?}",
813 field
814 );
815 debug_assert!(
816 is_valid_identifier(pk),
817 "pk must be a valid SQL identifier, got {:?}",
818 pk
819 );
820 format!("UPDATE {} SET {} = NOW() WHERE {} = ?", table, field, pk)
821}
822
823pub fn soft_delete_restore_sql(table: &str, field: &str, pk: &str) -> String {
840 debug_assert!(
841 is_valid_identifier(table),
842 "table must be a valid SQL identifier, got {:?}",
843 table
844 );
845 debug_assert!(
846 is_valid_identifier(field),
847 "field must be a valid SQL identifier, got {:?}",
848 field
849 );
850 debug_assert!(
851 is_valid_identifier(pk),
852 "pk must be a valid SQL identifier, got {:?}",
853 pk
854 );
855 format!("UPDATE {} SET {} = NULL WHERE {} = ?", table, field, pk)
856}
857
858pub const DEFAULT_TENANT_FIELD: &str = "app_id";
903
904pub fn tenant_filter_sql(field: &str, table: &str) -> String {
926 format!("{}.{} = ?", table, field)
927}
928
929pub fn tenant_filter_sql_no_table(field: &str) -> String {
951 format!("{} = ?", field)
952}
953
954pub fn is_tenant_aware(app_id: i64) -> bool {
980 app_id > 0
981}
982
983#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
996 fn test_all_events_count() {
997 assert_eq!(ALL_EVENTS.len(), 16, "16 事件完整列表");
998 }
999
1000 #[test]
1001 fn test_php_native_events_count() {
1002 assert_eq!(PHP_NATIVE_EVENTS.len(), 12, "PHP 原生 12 事件");
1003 }
1004
1005 #[test]
1006 fn test_extended_events_count() {
1007 assert_eq!(EXTENDED_EVENTS.len(), 4, "sz-orm-core 扩展 4 事件");
1008 }
1009
1010 #[test]
1011 fn test_all_events_no_duplicates() {
1012 let mut names: Vec<&str> = ALL_EVENTS.iter().map(|e| event_name(*e)).collect();
1014 names.sort();
1015 names.dedup();
1016 assert_eq!(names.len(), ALL_EVENTS.len(), "ALL_EVENTS 不应有重复");
1017 }
1018
1019 #[test]
1020 fn test_native_plus_extended_equals_all() {
1021 let mut all_names: Vec<&str> = ALL_EVENTS.iter().map(|e| event_name(*e)).collect();
1023 all_names.sort();
1024
1025 let mut combined: Vec<&str> = PHP_NATIVE_EVENTS
1026 .iter()
1027 .chain(EXTENDED_EVENTS.iter())
1028 .map(|e| event_name(*e))
1029 .collect();
1030 combined.sort();
1031
1032 assert_eq!(combined, all_names, "PHP_NATIVE + EXTENDED == ALL");
1033 }
1034
1035 #[test]
1040 fn test_event_name_php_style() {
1041 assert_eq!(event_name(HookEvent::BeforeInsert), "before_insert");
1043 assert_eq!(event_name(HookEvent::AfterInsert), "after_insert");
1044 assert_eq!(event_name(HookEvent::BeforeUpdate), "before_update");
1045 assert_eq!(event_name(HookEvent::AfterUpdate), "after_update");
1046 assert_eq!(event_name(HookEvent::BeforeDelete), "before_delete");
1047 assert_eq!(event_name(HookEvent::AfterDelete), "after_delete");
1048 assert_eq!(event_name(HookEvent::BeforeWrite), "before_write");
1049 assert_eq!(event_name(HookEvent::AfterWrite), "after_write");
1050 assert_eq!(event_name(HookEvent::BeforeSave), "before_save");
1051 assert_eq!(event_name(HookEvent::AfterSave), "after_save");
1052 assert_eq!(event_name(HookEvent::BeforeRestore), "before_restore");
1053 assert_eq!(event_name(HookEvent::AfterRestore), "after_restore");
1054 assert_eq!(event_name(HookEvent::BeforeFind), "before_find");
1055 assert_eq!(event_name(HookEvent::AfterFind), "after_find");
1056 assert_eq!(event_name(HookEvent::BeforeValidate), "before_validate");
1057 assert_eq!(event_name(HookEvent::AfterValidate), "after_validate");
1058 }
1059
1060 #[test]
1061 fn test_event_from_name_roundtrip() {
1062 for event in ALL_EVENTS.iter() {
1064 let name = event_name(*event);
1065 let back = event_from_name(name);
1066 assert_eq!(back, Some(*event), "事件 {:?} 往返映射失败", event);
1067 }
1068 }
1069
1070 #[test]
1071 fn test_event_from_name_unknown() {
1072 assert_eq!(event_from_name("unknown_event"), None);
1073 assert_eq!(event_from_name(""), None);
1074 assert_eq!(event_from_name("BeforeInsert"), None, "大小写敏感");
1075 assert_eq!(event_from_name("before-insert"), None, "需 snake_case");
1076 }
1077
1078 #[test]
1083 fn test_insert_order_aligns_php() {
1084 assert_eq!(
1090 INSERT_ORDER,
1091 [
1092 HookEvent::BeforeWrite,
1093 HookEvent::BeforeSave,
1094 HookEvent::BeforeValidate,
1095 HookEvent::AfterValidate,
1096 HookEvent::BeforeInsert,
1097 HookEvent::AfterInsert,
1098 HookEvent::AfterSave,
1099 HookEvent::AfterWrite,
1100 ]
1101 );
1102 let php_order = [
1104 HookEvent::BeforeWrite,
1105 HookEvent::BeforeInsert,
1106 HookEvent::AfterInsert,
1107 HookEvent::AfterWrite,
1108 ];
1109 let mut php_idx = 0;
1110 for event in INSERT_ORDER.iter() {
1111 if php_idx < php_order.len() && *event == php_order[php_idx] {
1112 php_idx += 1;
1113 }
1114 }
1115 assert_eq!(php_idx, php_order.len(), "PHP 原生顺序应作为子序列保留");
1116 }
1117
1118 #[test]
1119 fn test_update_order_aligns_php() {
1120 assert_eq!(
1123 UPDATE_ORDER,
1124 [
1125 HookEvent::BeforeWrite,
1126 HookEvent::BeforeSave,
1127 HookEvent::BeforeValidate,
1128 HookEvent::AfterValidate,
1129 HookEvent::BeforeUpdate,
1130 HookEvent::AfterUpdate,
1131 HookEvent::AfterSave,
1132 HookEvent::AfterWrite,
1133 ]
1134 );
1135 }
1136
1137 #[test]
1138 fn test_delete_order_aligns_php() {
1139 assert_eq!(
1141 DELETE_ORDER,
1142 [HookEvent::BeforeDelete, HookEvent::AfterDelete]
1143 );
1144 }
1145
1146 #[test]
1147 fn test_restore_order_aligns_php() {
1148 assert_eq!(
1150 RESTORE_ORDER,
1151 [HookEvent::BeforeRestore, HookEvent::AfterRestore]
1152 );
1153 }
1154
1155 #[test]
1156 fn test_find_order_aligns_php() {
1157 assert_eq!(FIND_ORDER, [HookEvent::BeforeFind, HookEvent::AfterFind]);
1159 }
1160
1161 #[test]
1166 fn test_recorder_empty() {
1167 let r = HookExecutionRecorder::new();
1168 assert!(r.is_empty());
1169 assert_eq!(r.len(), 0);
1170 assert_eq!(r.events(), Vec::<HookEvent>::new());
1171 assert_eq!(r.event_names(), Vec::<&str>::new());
1172 }
1173
1174 #[test]
1175 fn test_recorder_record_and_read() {
1176 let r = HookExecutionRecorder::new();
1177 r.record(HookEvent::BeforeInsert);
1178 r.record(HookEvent::AfterInsert);
1179 assert_eq!(r.len(), 2);
1180 assert_eq!(
1181 r.events(),
1182 vec![HookEvent::BeforeInsert, HookEvent::AfterInsert]
1183 );
1184 assert_eq!(r.event_names(), vec!["before_insert", "after_insert"]);
1185 }
1186
1187 #[test]
1188 fn test_recorder_clear() {
1189 let r = HookExecutionRecorder::new();
1190 r.record(HookEvent::BeforeInsert);
1191 r.clear();
1192 assert!(r.is_empty());
1193 }
1194
1195 #[test]
1196 fn test_recorder_assert_order_ok() {
1197 let r = HookExecutionRecorder::new();
1198 r.record(HookEvent::BeforeInsert);
1199 r.record(HookEvent::AfterInsert);
1200 let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
1201 assert!(result.is_ok());
1202 }
1203
1204 #[test]
1205 fn test_recorder_assert_order_mismatch_count() {
1206 let r = HookExecutionRecorder::new();
1207 r.record(HookEvent::BeforeInsert);
1208 let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
1209 assert!(result.is_err());
1210 assert!(result.unwrap_err().contains("数量不匹配"));
1211 }
1212
1213 #[test]
1214 fn test_recorder_assert_order_mismatch_value() {
1215 let r = HookExecutionRecorder::new();
1216 r.record(HookEvent::BeforeInsert);
1217 r.record(HookEvent::BeforeUpdate);
1218 let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
1219 assert!(result.is_err());
1220 let err = result.unwrap_err();
1221 assert!(err.contains("顺序不一致"));
1222 assert!(err.contains("after_insert"));
1223 }
1224
1225 #[test]
1230 fn test_validate_insert_order() {
1231 let registry = HookRegistry::new();
1232 let result = validate_insert_order(®istry);
1233 assert!(result.is_ok(), "INSERT 顺序验证应通过:{:?}", result);
1234 }
1235
1236 #[test]
1237 fn test_validate_update_order() {
1238 let registry = HookRegistry::new();
1239 let result = validate_update_order(®istry);
1240 assert!(result.is_ok(), "UPDATE 顺序验证应通过:{:?}", result);
1241 }
1242
1243 #[test]
1244 fn test_validate_delete_order() {
1245 let registry = HookRegistry::new();
1246 let result = validate_delete_order(®istry);
1247 assert!(result.is_ok(), "DELETE 顺序验证应通过:{:?}", result);
1248 }
1249
1250 #[test]
1251 fn test_validate_restore_order() {
1252 let registry = HookRegistry::new();
1253 let result = validate_restore_order(®istry);
1254 assert!(result.is_ok(), "RESTORE 顺序验证应通过:{:?}", result);
1255 }
1256
1257 #[test]
1258 fn test_validate_find_order() {
1259 let registry = HookRegistry::new();
1260 let result = validate_find_order(®istry);
1261 assert!(result.is_ok(), "FIND 顺序验证应通过:{:?}", result);
1262 }
1263
1264 #[test]
1269 fn test_all_16_events_registerable_and_dispatchable() {
1270 let registry = HookRegistry::new();
1271 let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
1272
1273 for event in ALL_EVENTS.iter() {
1274 let c = Arc::clone(&counter);
1275 registry.register(
1276 *event,
1277 Arc::new(move |_ctx| {
1278 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1279 Ok(())
1280 }),
1281 );
1282 }
1283
1284 let ctx = HookContext::new();
1285 for event in ALL_EVENTS.iter() {
1286 registry.dispatch(*event, &ctx).expect("dispatch 应成功");
1287 }
1288
1289 assert_eq!(
1290 counter.load(std::sync::atomic::Ordering::SeqCst),
1291 16,
1292 "16 事件全部应被触发一次"
1293 );
1294 }
1295
1296 #[test]
1297 fn test_all_16_events_countable() {
1298 let registry = HookRegistry::new();
1299 for event in ALL_EVENTS.iter() {
1300 registry.register(*event, Arc::new(|_ctx| Ok(())));
1301 }
1302 for event in ALL_EVENTS.iter() {
1303 assert_eq!(
1304 registry.count(*event),
1305 1,
1306 "事件 {:?} 应注册 1 个钩子",
1307 event
1308 );
1309 }
1310 }
1311
1312 #[test]
1318 fn test_r5_php_native_12_events_available() {
1319 for event in PHP_NATIVE_EVENTS.iter() {
1322 let name = event_name(*event);
1323 let back = event_from_name(name);
1324 assert_eq!(
1325 back,
1326 Some(*event),
1327 "PHP 原生事件 {:?} 应在 sz-rust 中可用",
1328 name
1329 );
1330 }
1331 }
1332
1333 #[test]
1337 fn test_r5_php_insert_order_preserved() {
1338 let php_native_order = [
1339 HookEvent::BeforeWrite,
1340 HookEvent::BeforeInsert,
1341 HookEvent::AfterInsert,
1342 HookEvent::AfterWrite,
1343 ];
1344 let mut php_idx = 0;
1345 for event in INSERT_ORDER.iter() {
1346 if php_idx < php_native_order.len() && *event == php_native_order[php_idx] {
1347 php_idx += 1;
1348 }
1349 }
1350 assert_eq!(
1351 php_idx,
1352 php_native_order.len(),
1353 "PHP 原生 INSERT 顺序应作为 sz-rust 扩展顺序的子序列保留"
1354 );
1355 }
1356
1357 #[test]
1359 fn test_r5_php_update_order_preserved() {
1360 let php_native_order = [
1361 HookEvent::BeforeWrite,
1362 HookEvent::BeforeUpdate,
1363 HookEvent::AfterUpdate,
1364 HookEvent::AfterWrite,
1365 ];
1366 let mut php_idx = 0;
1367 for event in UPDATE_ORDER.iter() {
1368 if php_idx < php_native_order.len() && *event == php_native_order[php_idx] {
1369 php_idx += 1;
1370 }
1371 }
1372 assert_eq!(
1373 php_idx,
1374 php_native_order.len(),
1375 "PHP 原生 UPDATE 顺序应作为 sz-rust 扩展顺序的子序列保留"
1376 );
1377 }
1378
1379 #[test]
1381 fn test_r5_php_delete_order_exact() {
1382 assert_eq!(
1383 DELETE_ORDER,
1384 [HookEvent::BeforeDelete, HookEvent::AfterDelete],
1385 "DELETE 顺序应与 PHP 完全一致(无扩展)"
1386 );
1387 }
1388
1389 #[test]
1391 fn test_r5_php_restore_order_exact() {
1392 assert_eq!(
1393 RESTORE_ORDER,
1394 [HookEvent::BeforeRestore, HookEvent::AfterRestore],
1395 "RESTORE 顺序应与 PHP 完全一致(无扩展)"
1396 );
1397 }
1398
1399 #[test]
1401 fn test_r5_php_find_order_exact() {
1402 assert_eq!(
1403 FIND_ORDER,
1404 [HookEvent::BeforeFind, HookEvent::AfterFind],
1405 "FIND 顺序应与 PHP 完全一致(无扩展)"
1406 );
1407 }
1408
1409 #[test]
1414 fn test_r5_php_actual_usage_before_insert_via_registry() {
1415 let registry = HookRegistry::new();
1420 let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1421 let c = Arc::clone(&called);
1422 registry.register(
1423 HookEvent::BeforeInsert,
1424 Arc::new(move |_ctx| {
1425 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1426 Ok(())
1427 }),
1428 );
1429 let ctx = HookContext::new();
1430 registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
1431 assert_eq!(
1432 called.load(std::sync::atomic::Ordering::SeqCst),
1433 1,
1434 "before_insert 钩子应被触发"
1435 );
1436 }
1437
1438 #[test]
1445 fn test_r5_php_business_level_before_insert_convention_documented() {
1446 let registry = HookRegistry::new();
1450 let recorder = Arc::new(HookExecutionRecorder::new());
1451 let r = Arc::clone(&recorder);
1452 registry.register(
1453 HookEvent::BeforeInsert,
1454 Arc::new(move |_ctx| {
1455 r.record(HookEvent::BeforeInsert);
1456 Ok(())
1457 }),
1458 );
1459 let ctx = HookContext::new();
1460 registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
1461 assert_eq!(recorder.events(), vec![HookEvent::BeforeInsert]);
1462 }
1463
1464 #[test]
1469 fn test_hook_context_re_exported() {
1470 let ctx = HookContext::new()
1471 .with_tenant(42)
1472 .with_operator(1)
1473 .with_timestamp(1700000000);
1474 assert_eq!(ctx.tenant_id, Some(42));
1475 assert_eq!(ctx.operator_id, Some(1));
1476 assert_eq!(ctx.timestamp, 1700000000);
1477 }
1478
1479 #[test]
1480 fn test_hook_context_metadata() {
1481 let mut ctx = HookContext::new();
1482 ctx.set_meta("source", "api");
1483 ctx.set_meta("ip", "127.0.0.1");
1484 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1485 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1486 assert_eq!(ctx.get_meta("missing"), None);
1487 }
1488
1489 #[test]
1494 fn test_hook_event_is_before() {
1495 assert!(HookEvent::BeforeInsert.is_before());
1496 assert!(HookEvent::BeforeWrite.is_before());
1497 assert!(HookEvent::BeforeSave.is_before());
1498 assert!(HookEvent::BeforeValidate.is_before());
1499 assert!(HookEvent::BeforeFind.is_before());
1500 assert!(HookEvent::BeforeRestore.is_before());
1501 assert!(!HookEvent::AfterInsert.is_before());
1502 }
1503
1504 #[test]
1505 fn test_hook_event_is_after() {
1506 assert!(HookEvent::AfterInsert.is_after());
1507 assert!(HookEvent::AfterWrite.is_after());
1508 assert!(HookEvent::AfterSave.is_after());
1509 assert!(HookEvent::AfterValidate.is_after());
1510 assert!(HookEvent::AfterFind.is_after());
1511 assert!(HookEvent::AfterRestore.is_after());
1512 assert!(!HookEvent::BeforeInsert.is_after());
1513 }
1514
1515 #[test]
1516 fn test_hook_event_is_write_level() {
1517 assert!(HookEvent::BeforeWrite.is_write_level());
1518 assert!(HookEvent::AfterWrite.is_write_level());
1519 assert!(HookEvent::BeforeSave.is_write_level());
1520 assert!(HookEvent::AfterSave.is_write_level());
1521 assert!(!HookEvent::BeforeInsert.is_write_level());
1522 assert!(!HookEvent::BeforeFind.is_write_level());
1523 }
1524
1525 #[test]
1526 fn test_hook_event_is_find_level() {
1527 assert!(HookEvent::BeforeFind.is_find_level());
1528 assert!(HookEvent::AfterFind.is_find_level());
1529 assert!(!HookEvent::BeforeInsert.is_find_level());
1530 }
1531
1532 #[test]
1533 fn test_hook_event_is_validate_level() {
1534 assert!(HookEvent::BeforeValidate.is_validate_level());
1535 assert!(HookEvent::AfterValidate.is_validate_level());
1536 assert!(!HookEvent::BeforeInsert.is_validate_level());
1537 }
1538
1539 #[test]
1540 fn test_hook_event_is_fine_grained() {
1541 for event in EXTENDED_EVENTS.iter() {
1543 assert!(event.is_fine_grained(), "扩展事件 {:?} 应为细粒度", event);
1544 }
1545 assert!(!HookEvent::BeforeInsert.is_fine_grained());
1547 assert!(!HookEvent::AfterInsert.is_fine_grained());
1548 assert!(!HookEvent::BeforeUpdate.is_fine_grained());
1549 assert!(!HookEvent::AfterUpdate.is_fine_grained());
1550 assert!(!HookEvent::BeforeDelete.is_fine_grained());
1551 assert!(!HookEvent::AfterDelete.is_fine_grained());
1552 }
1553
1554 #[test]
1559 fn test_hook_registry_short_circuit_on_error() {
1560 use sz_rust_orm_facade::DbError;
1562 let registry = HookRegistry::new();
1563 let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1564
1565 let c1 = Arc::clone(&called);
1566 registry.register(
1567 HookEvent::BeforeInsert,
1568 Arc::new(move |_ctx| {
1569 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1570 Ok(())
1571 }),
1572 );
1573
1574 registry.register(
1575 HookEvent::BeforeInsert,
1576 Arc::new(|_ctx| Err(DbError::Hook("second hook failed".into()))),
1577 );
1578
1579 let c3 = Arc::clone(&called);
1580 registry.register(
1581 HookEvent::BeforeInsert,
1582 Arc::new(move |_ctx| {
1583 c3.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1584 Ok(())
1585 }),
1586 );
1587
1588 let ctx = HookContext::new();
1589 let result = registry.dispatch(HookEvent::BeforeInsert, &ctx);
1590 assert!(result.is_err());
1591 assert_eq!(
1592 called.load(std::sync::atomic::Ordering::SeqCst),
1593 1,
1594 "第二个钩子失败后第三个不应执行"
1595 );
1596 }
1597
1598 #[test]
1603 fn test_hook_registry_clear() {
1604 let registry = HookRegistry::new();
1605 registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
1606 assert_eq!(registry.count(HookEvent::BeforeInsert), 1);
1607 registry.clear(HookEvent::BeforeInsert);
1608 assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
1609 }
1610
1611 #[test]
1612 fn test_hook_registry_clear_all() {
1613 let registry = HookRegistry::new();
1614 registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
1615 registry.register(HookEvent::AfterInsert, Arc::new(|_ctx| Ok(())));
1616 registry.register(HookEvent::BeforeUpdate, Arc::new(|_ctx| Ok(())));
1617 registry.clear_all();
1618 assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
1619 assert_eq!(registry.count(HookEvent::AfterInsert), 0);
1620 assert_eq!(registry.count(HookEvent::BeforeUpdate), 0);
1621 }
1622
1623 #[test]
1624 fn test_hook_registry_dispatch_no_hooks() {
1625 let registry = HookRegistry::new();
1626 let ctx = HookContext::new();
1627 assert!(registry.dispatch(HookEvent::BeforeInsert, &ctx).is_ok());
1629 }
1630
1631 #[test]
1636 fn test_scope_registry_enable_disable() {
1637 let registry = ScopeRegistry::new();
1638 assert!(registry.is_enabled("soft_delete"));
1639 assert!(registry.is_enabled("tenant"));
1640
1641 registry.disable("soft_delete");
1642 assert!(!registry.is_enabled("soft_delete"));
1643 assert!(registry.is_enabled("tenant"));
1644
1645 registry.enable("soft_delete");
1646 assert!(registry.is_enabled("soft_delete"));
1647 }
1648
1649 #[test]
1650 fn test_scope_registry_without_scope() {
1651 let registry = ScopeRegistry::new();
1652 assert!(registry.is_enabled("soft_delete"));
1653
1654 let result = registry.without_scope("soft_delete", || {
1655 assert!(!registry.is_enabled("soft_delete"));
1656 42
1657 });
1658
1659 assert_eq!(result, 42);
1660 assert!(registry.is_enabled("soft_delete"));
1661 }
1662
1663 #[test]
1668 fn test_hook_context_builder_tenant_id() {
1669 let ctx = hook_context().with_tenant(42);
1671 assert_eq!(ctx.tenant_id, Some(42));
1672 assert_eq!(ctx.operator_id, None);
1673 assert_eq!(ctx.timestamp, 0);
1674 }
1675
1676 #[test]
1677 fn test_hook_context_builder_operator_id() {
1678 let ctx = hook_context().with_operator(1);
1680 assert_eq!(ctx.tenant_id, None);
1681 assert_eq!(ctx.operator_id, Some(1));
1682 assert_eq!(ctx.timestamp, 0);
1683 }
1684
1685 #[test]
1686 fn test_hook_context_builder_timestamp() {
1687 let ctx = hook_context().with_timestamp(1700000000);
1689 assert_eq!(ctx.tenant_id, None);
1690 assert_eq!(ctx.operator_id, None);
1691 assert_eq!(ctx.timestamp, 1700000000);
1692 }
1693
1694 #[test]
1695 fn test_hook_context_builder_metadata_with_meta() {
1696 let ctx = hook_context()
1698 .with_meta("source", "api")
1699 .with_meta("ip", "127.0.0.1")
1700 .with_meta("trace_id", "abc123");
1701 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1702 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1703 assert_eq!(ctx.get_meta("trace_id"), Some(&"abc123".to_string()));
1704 assert_eq!(ctx.get_meta("missing"), None);
1705 assert_eq!(ctx.metadata.len(), 3);
1706 }
1707
1708 #[test]
1709 fn test_hook_context_builder_metadata_with_metas_vec() {
1710 let entries = vec![
1712 ("source".to_string(), "api".to_string()),
1713 ("ip".to_string(), "127.0.0.1".to_string()),
1714 ("trace_id".to_string(), "abc123".to_string()),
1715 ];
1716 let ctx = hook_context().with_metas(entries);
1717 assert_eq!(ctx.metadata.len(), 3);
1718 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1719 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1720 assert_eq!(ctx.get_meta("trace_id"), Some(&"abc123".to_string()));
1721 }
1722
1723 #[test]
1724 fn test_hook_context_builder_metadata_with_metas_array() {
1725 let ctx = hook_context().with_metas([("source", "api"), ("ip", "127.0.0.1")]);
1727 assert_eq!(ctx.metadata.len(), 2);
1728 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1729 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1730 }
1731
1732 #[test]
1733 fn test_hook_context_builder_full_chain() {
1734 let ctx = hook_context()
1736 .with_tenant(42)
1737 .with_operator(1)
1738 .with_timestamp(1700000000)
1739 .with_meta("source", "api")
1740 .with_meta("ip", "127.0.0.1");
1741 assert_eq!(ctx.tenant_id, Some(42));
1742 assert_eq!(ctx.operator_id, Some(1));
1743 assert_eq!(ctx.timestamp, 1700000000);
1744 assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
1745 assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
1746 assert_eq!(ctx.metadata.len(), 2);
1747 }
1748
1749 #[test]
1750 fn test_hook_context_with_meta_overwrite() {
1751 let ctx = hook_context()
1753 .with_meta("source", "api")
1754 .with_meta("source", "web"); assert_eq!(ctx.get_meta("source"), Some(&"web".to_string()));
1756 assert_eq!(ctx.metadata.len(), 1);
1757 }
1758
1759 #[test]
1760 fn test_hook_context_with_metas_empty() {
1761 let ctx = hook_context().with_metas(Vec::<(String, String)>::new());
1763 assert_eq!(ctx.metadata.len(), 0);
1764 assert!(ctx.metadata.is_empty());
1765 }
1766
1767 #[test]
1768 fn test_hook_context_set_meta_vs_with_meta() {
1769 let mut ctx1 = HookContext::new();
1771 ctx1.set_meta("key", "value1");
1772 ctx1.set_meta("key2", "value2");
1773
1774 let ctx2 = hook_context()
1775 .with_meta("key", "value1")
1776 .with_meta("key2", "value2");
1777
1778 assert_eq!(ctx1.metadata, ctx2.metadata);
1779 }
1780
1781 #[test]
1786 fn test_hook_context_convenience_empty() {
1787 let ctx1 = hook_context();
1789 let ctx2 = HookContext::new();
1790 assert_eq!(ctx1.tenant_id, ctx2.tenant_id);
1791 assert_eq!(ctx1.operator_id, ctx2.operator_id);
1792 assert_eq!(ctx1.timestamp, ctx2.timestamp);
1793 assert_eq!(ctx1.metadata, ctx2.metadata);
1794 }
1795
1796 #[test]
1797 fn test_hook_context_convenience_with_tenant() {
1798 let ctx1 = hook_context_with_tenant(42);
1800 let ctx2 = hook_context().with_tenant(42);
1801 assert_eq!(ctx1.tenant_id, Some(42));
1802 assert_eq!(ctx1.tenant_id, ctx2.tenant_id);
1803 }
1804
1805 #[test]
1806 fn test_hook_context_convenience_with_operator() {
1807 let ctx1 = hook_context_with_operator(1);
1809 let ctx2 = hook_context().with_operator(1);
1810 assert_eq!(ctx1.operator_id, Some(1));
1811 assert_eq!(ctx1.operator_id, ctx2.operator_id);
1812 }
1813
1814 #[test]
1815 fn test_hook_context_convenience_from_meta_vec() {
1816 let headers = vec![
1818 ("x-trace-id".to_string(), "abc123".to_string()),
1819 ("x-source".to_string(), "api".to_string()),
1820 ];
1821 let ctx = hook_context_from_meta(headers);
1822 assert_eq!(ctx.get_meta("x-trace-id"), Some(&"abc123".to_string()));
1823 assert_eq!(ctx.get_meta("x-source"), Some(&"api".to_string()));
1824 assert_eq!(ctx.metadata.len(), 2);
1825 }
1826
1827 #[test]
1828 fn test_hook_context_convenience_from_meta_array() {
1829 let ctx = hook_context_from_meta([("k1", "v1"), ("k2", "v2")]);
1831 assert_eq!(ctx.get_meta("k1"), Some(&"v1".to_string()));
1832 assert_eq!(ctx.get_meta("k2"), Some(&"v2".to_string()));
1833 }
1834
1835 #[test]
1836 fn test_hook_context_convenience_from_meta_empty() {
1837 let ctx = hook_context_from_meta(Vec::<(String, String)>::new());
1839 assert!(ctx.metadata.is_empty());
1840 }
1841
1842 #[test]
1852 fn test_r5_php_trigger_context_passing() {
1853 let ctx = hook_context()
1856 .with_operator(1)
1857 .with_timestamp(1700000000)
1858 .with_meta("action", "insert")
1859 .with_meta("model_class", "Worklogs");
1860
1861 assert_eq!(ctx.operator_id, Some(1), "操作人 ID 应可设置");
1863 assert_eq!(ctx.timestamp, 1700000000, "时间戳应可设置");
1864 assert_eq!(
1865 ctx.get_meta("action"),
1866 Some(&"insert".to_string()),
1867 "action 元数据应可设置"
1868 );
1869 assert_eq!(
1870 ctx.get_meta("model_class"),
1871 Some(&"Worklogs".to_string()),
1872 "model_class 元数据应可设置"
1873 );
1874 }
1875
1876 #[test]
1881 fn test_r5_php_auto_fill_timestamp_via_context() {
1882 let now = 1700000000_u64;
1883 let ctx = hook_context().with_timestamp(now);
1884
1885 let create_time = ctx.timestamp;
1887 let update_time = ctx.timestamp;
1888
1889 assert_eq!(create_time, now, "create_time 应从 ctx.timestamp 获取");
1890 assert_eq!(update_time, now, "update_time 应从 ctx.timestamp 获取");
1891 }
1892
1893 #[test]
1897 fn test_r5_php_worklogs_stat_day_via_context_meta() {
1898 let ctx = hook_context()
1899 .with_timestamp(1700000000)
1900 .with_meta("stat_day", "20231114");
1901
1902 assert_eq!(ctx.get_meta("stat_day"), Some(&"20231114".to_string()));
1903 }
1904
1905 #[test]
1909 fn test_r5_php_tenant_context_via_hook_context() {
1910 let ctx = hook_context_with_tenant(42);
1911
1912 assert_eq!(ctx.tenant_id, Some(42), "租户 ID 应可设置");
1913 }
1914
1915 #[test]
1919 fn test_r5_php_operator_context_via_hook_context() {
1920 let ctx = hook_context_with_operator(1);
1921
1922 assert_eq!(ctx.operator_id, Some(1), "操作人 ID 应可设置");
1923 }
1924
1925 #[test]
1930 fn test_r5_php_event_listen_context_via_hook_registry() {
1931 let registry = HookRegistry::new();
1932 let captured_operator_id = Arc::new(std::sync::Mutex::new(None::<i64>));
1933
1934 let c = Arc::clone(&captured_operator_id);
1935 registry.register(
1936 HookEvent::BeforeInsert,
1937 Arc::new(move |ctx| {
1938 *c.lock().unwrap() = ctx.operator_id;
1939 Ok(())
1940 }),
1941 );
1942
1943 let ctx = hook_context_with_operator(42);
1944 registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
1945
1946 assert_eq!(
1947 *captured_operator_id.lock().unwrap(),
1948 Some(42),
1949 "运行时钩子应能读取 ctx.operator_id"
1950 );
1951 }
1952
1953 #[test]
1957 fn test_r5_php_audit_log_metadata_via_hook_context() {
1958 let ctx = hook_context()
1959 .with_operator(1)
1960 .with_meta("ip", "192.168.1.100")
1961 .with_meta("ua", "Mozilla/5.0")
1962 .with_meta("source", "web")
1963 .with_meta("trace_id", "abc-123-def");
1964
1965 assert_eq!(ctx.get_meta("ip"), Some(&"192.168.1.100".to_string()));
1967 assert_eq!(ctx.get_meta("ua"), Some(&"Mozilla/5.0".to_string()));
1968 assert_eq!(ctx.get_meta("source"), Some(&"web".to_string()));
1969 assert_eq!(ctx.get_meta("trace_id"), Some(&"abc-123-def".to_string()));
1970 assert_eq!(ctx.metadata.len(), 4);
1971 }
1972
1973 #[test]
1977 fn test_r5_php_batch_headers_via_hook_context_from_meta() {
1978 let headers = vec![
1979 ("x-request-id".to_string(), "req-001".to_string()),
1980 ("x-trace-id".to_string(), "trace-001".to_string()),
1981 ("x-tenant-id".to_string(), "42".to_string()),
1982 ("x-operator-id".to_string(), "1".to_string()),
1983 ];
1984 let ctx = hook_context_from_meta(headers);
1985
1986 assert_eq!(ctx.get_meta("x-request-id"), Some(&"req-001".to_string()));
1987 assert_eq!(ctx.get_meta("x-trace-id"), Some(&"trace-001".to_string()));
1988 assert_eq!(ctx.get_meta("x-tenant-id"), Some(&"42".to_string()));
1989 assert_eq!(ctx.get_meta("x-operator-id"), Some(&"1".to_string()));
1990 assert_eq!(ctx.metadata.len(), 4);
1991 }
1992
1993 #[test]
1998 fn test_default_soft_delete_field() {
1999 assert_eq!(DEFAULT_SOFT_DELETE_FIELD, "delete_time");
2001 }
2002
2003 #[test]
2004 fn test_soft_delete_filter_sql_default_field() {
2005 let sql = soft_delete_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
2007 assert_eq!(sql, "delete_time IS NULL");
2008 }
2009
2010 #[test]
2011 fn test_soft_delete_filter_sql_custom_field() {
2012 let sql = soft_delete_filter_sql("deleted_at");
2014 assert_eq!(sql, "deleted_at IS NULL");
2015 }
2016
2017 #[test]
2018 fn test_only_trashed_filter_sql_default_field() {
2019 let sql = only_trashed_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
2021 assert_eq!(sql, "delete_time IS NOT NULL");
2022 }
2023
2024 #[test]
2025 fn test_only_trashed_filter_sql_custom_field() {
2026 let sql = only_trashed_filter_sql("deleted_at");
2027 assert_eq!(sql, "deleted_at IS NOT NULL");
2028 }
2029
2030 #[test]
2031 fn test_is_soft_deleted_null() {
2032 assert!(!is_soft_deleted(None));
2034 }
2035
2036 #[test]
2037 fn test_is_soft_deleted_empty_string() {
2038 assert!(!is_soft_deleted(Some("")));
2040 }
2041
2042 #[test]
2043 fn test_is_soft_deleted_datetime_value() {
2044 assert!(is_soft_deleted(Some("2026-07-21 10:00:00")));
2046 }
2047
2048 #[test]
2049 fn test_is_soft_deleted_timestamp_value() {
2050 assert!(is_soft_deleted(Some("1700000000")));
2052 }
2053
2054 #[test]
2055 fn test_is_soft_deleted_zero_string() {
2056 assert!(is_soft_deleted(Some("0")));
2061 }
2062
2063 #[test]
2064 fn test_soft_delete_update_sql_default() {
2065 let sql = soft_delete_update_sql("users", DEFAULT_SOFT_DELETE_FIELD, "id");
2067 assert_eq!(sql, "UPDATE users SET delete_time = NOW() WHERE id = ?");
2068 }
2069
2070 #[test]
2071 fn test_soft_delete_update_sql_custom() {
2072 let sql = soft_delete_update_sql("orders", "deleted_at", "order_id");
2074 assert_eq!(
2075 sql,
2076 "UPDATE orders SET deleted_at = NOW() WHERE order_id = ?"
2077 );
2078 }
2079
2080 #[test]
2081 fn test_soft_delete_restore_sql_default() {
2082 let sql = soft_delete_restore_sql("users", DEFAULT_SOFT_DELETE_FIELD, "id");
2084 assert_eq!(sql, "UPDATE users SET delete_time = NULL WHERE id = ?");
2085 }
2086
2087 #[test]
2088 fn test_soft_delete_restore_sql_custom() {
2089 let sql = soft_delete_restore_sql("orders", "deleted_at", "order_id");
2090 assert_eq!(
2091 sql,
2092 "UPDATE orders SET deleted_at = NULL WHERE order_id = ?"
2093 );
2094 }
2095
2096 #[test]
2102 fn test_r5_php_soft_delete_default_field_name() {
2103 assert_eq!(DEFAULT_SOFT_DELETE_FIELD, "delete_time");
2108 assert_ne!(DEFAULT_SOFT_DELETE_FIELD, "deleted_at");
2109 }
2110
2111 #[test]
2113 fn test_r5_php_with_no_trashed_default_condition() {
2114 let sz_rust_sql = soft_delete_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
2118 assert_eq!(sz_rust_sql, "delete_time IS NULL");
2119 let _ = std::marker::PhantomData::<SoftDeleteScope>;
2121 }
2122
2123 #[test]
2125 fn test_r5_php_trashed_logic() {
2126 assert!(!is_soft_deleted(None), "NULL → 未软删除");
2129 assert!(!is_soft_deleted(Some("")), "空字符串 → 未软删除");
2130 assert!(
2131 is_soft_deleted(Some("2026-07-21 10:00:00")),
2132 "有值 → 已软删除"
2133 );
2134 }
2135
2136 #[test]
2138 fn test_r5_php_only_trashed_condition() {
2139 let sql = only_trashed_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
2140 assert_eq!(sql, "delete_time IS NOT NULL");
2141 }
2142
2143 #[test]
2145 fn test_r5_php_delete_sql_format() {
2146 let sql = soft_delete_update_sql("users", "delete_time", "id");
2148 assert!(sql.contains("UPDATE users"));
2149 assert!(sql.contains("SET delete_time = NOW()"));
2150 assert!(sql.contains("WHERE id = ?"));
2151 }
2152
2153 #[test]
2155 fn test_r5_php_restore_sql_format() {
2156 let sql = soft_delete_restore_sql("users", "delete_time", "id");
2158 assert!(sql.contains("UPDATE users"));
2159 assert!(sql.contains("SET delete_time = NULL"));
2160 assert!(sql.contains("WHERE id = ?"));
2161 }
2162
2163 #[test]
2165 fn test_r5_php_basemodel_no_soft_delete_trait() {
2166 let _ = std::marker::PhantomData::<SoftDeleteScope>;
2172 }
2173
2174 #[test]
2176 fn test_r5_php_upload_file_disable_soft_delete() {
2177 struct UploadFile; let _ = std::marker::PhantomData::<UploadFile>;
2183 }
2185
2186 #[test]
2191 fn test_default_tenant_field() {
2192 assert_eq!(DEFAULT_TENANT_FIELD, "app_id");
2194 assert_ne!(DEFAULT_TENANT_FIELD, "tenant_id");
2195 }
2196
2197 #[test]
2198 fn test_tenant_filter_sql_default_field() {
2199 let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "users");
2201 assert_eq!(sql, "users.app_id = ?");
2202 }
2203
2204 #[test]
2205 fn test_tenant_filter_sql_custom_field() {
2206 let sql = tenant_filter_sql("tenant_id", "orders");
2208 assert_eq!(sql, "orders.tenant_id = ?");
2209 }
2210
2211 #[test]
2212 fn test_tenant_filter_sql_with_alias() {
2213 let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "u");
2215 assert_eq!(sql, "u.app_id = ?");
2216 }
2217
2218 #[test]
2219 fn test_tenant_filter_sql_no_table_default() {
2220 let sql = tenant_filter_sql_no_table(DEFAULT_TENANT_FIELD);
2222 assert_eq!(sql, "app_id = ?");
2223 }
2224
2225 #[test]
2226 fn test_tenant_filter_sql_no_table_custom() {
2227 let sql = tenant_filter_sql_no_table("tenant_id");
2229 assert_eq!(sql, "tenant_id = ?");
2230 }
2231
2232 #[test]
2233 fn test_is_tenant_aware_zero() {
2234 assert!(!is_tenant_aware(0));
2236 }
2237
2238 #[test]
2239 fn test_is_tenant_aware_negative() {
2240 assert!(!is_tenant_aware(-1));
2242 assert!(!is_tenant_aware(-100));
2243 }
2244
2245 #[test]
2246 fn test_is_tenant_aware_positive() {
2247 assert!(is_tenant_aware(1));
2249 assert!(is_tenant_aware(42));
2250 assert!(is_tenant_aware(10000));
2251 }
2252
2253 #[test]
2254 fn test_is_tenant_aware_max_i64() {
2255 assert!(is_tenant_aware(i64::MAX));
2257 }
2258
2259 #[test]
2265 fn test_r5_php_tenant_field_name_app_id() {
2266 assert_eq!(DEFAULT_TENANT_FIELD, "app_id");
2273 assert_ne!(DEFAULT_TENANT_FIELD, "tenant_id");
2274 }
2275
2276 #[test]
2278 fn test_r5_php_scope_app_id_condition() {
2279 let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "sz_user");
2283 assert_eq!(sql, "sz_user.app_id = ?");
2284 assert!(sql.contains(".app_id"));
2286 }
2287
2288 #[test]
2290 fn test_r5_php_app_id_gt_zero_check() {
2291 assert!(!is_tenant_aware(0), "app_id=0 不启用多租户过滤");
2294 assert!(!is_tenant_aware(-1), "app_id=-1 不启用多租户过滤");
2295 assert!(is_tenant_aware(1), "app_id=1 启用多租户过滤");
2296 assert!(is_tenant_aware(42), "app_id=42 启用多租户过滤");
2297 }
2298
2299 #[test]
2301 fn test_r5_php_global_scope_declaration() {
2302 let scope_name = "app_id"; let registry = ScopeRegistry::new();
2307 registry.enable(scope_name);
2309 assert!(registry.is_enabled(scope_name), "app_id 全局作用域已启用");
2310 registry.disable(scope_name);
2312 assert!(!registry.is_enabled(scope_name), "app_id 全局作用域已禁用");
2313 }
2314
2315 #[test]
2317 fn test_r5_php_module_based_app_id_binding() {
2318 let ctx = hook_context_with_tenant(42);
2323 assert_eq!(ctx.tenant_id, Some(42));
2324 let app_id = ctx.tenant_id.unwrap_or(0);
2326 assert!(is_tenant_aware(app_id));
2327 }
2328
2329 #[test]
2331 fn test_r5_php_tenant_scope_vs_sz_orm_core() {
2332 let php_style = tenant_filter_sql_no_table(DEFAULT_TENANT_FIELD);
2336 assert_eq!(php_style, "app_id = ?");
2337 let sz_orm_style = tenant_filter_sql_no_table("tenant_id");
2338 assert_eq!(sz_orm_style, "tenant_id = ?");
2339 assert_ne!(php_style, sz_orm_style);
2341 }
2342
2343 #[test]
2345 fn test_r5_php_tenant_context_passing() {
2346 let ctx1 = hook_context_with_tenant(100);
2349 let ctx2 = hook_context_with_tenant(200);
2350 assert_ne!(ctx1.tenant_id, ctx2.tenant_id);
2352 assert_eq!(ctx1.tenant_id, Some(100));
2353 assert_eq!(ctx2.tenant_id, Some(200));
2354 }
2355
2356 #[test]
2358 fn test_r5_php_no_cross_tenant_query_when_app_id_zero() {
2359 let app_id = 0;
2363 assert!(!is_tenant_aware(app_id));
2364 }
2367}