pub use sz_rust_orm_facade::hooks::{
GlobalScope, HookContext, HookDispatcher, HookEvent, HookFn, HookRegistry, HookResult,
Hookable, ScopeRegistry, SoftDelete, SoftDeleteScope, TenantModel, TenantScope,
};
pub const ALL_EVENTS: [HookEvent; 16] = [
HookEvent::BeforeInsert,
HookEvent::AfterInsert,
HookEvent::BeforeUpdate,
HookEvent::AfterUpdate,
HookEvent::BeforeDelete,
HookEvent::AfterDelete,
HookEvent::BeforeFind,
HookEvent::AfterFind,
HookEvent::BeforeRestore,
HookEvent::AfterRestore,
HookEvent::BeforeWrite,
HookEvent::AfterWrite,
HookEvent::BeforeSave,
HookEvent::AfterSave,
HookEvent::BeforeValidate,
HookEvent::AfterValidate,
];
pub const PHP_NATIVE_EVENTS: [HookEvent; 12] = [
HookEvent::BeforeInsert,
HookEvent::AfterInsert,
HookEvent::BeforeUpdate,
HookEvent::AfterUpdate,
HookEvent::BeforeDelete,
HookEvent::AfterDelete,
HookEvent::BeforeFind,
HookEvent::AfterFind,
HookEvent::BeforeRestore,
HookEvent::AfterRestore,
HookEvent::BeforeWrite,
HookEvent::AfterWrite,
];
pub const EXTENDED_EVENTS: [HookEvent; 4] = [
HookEvent::BeforeSave,
HookEvent::AfterSave,
HookEvent::BeforeValidate,
HookEvent::AfterValidate,
];
pub fn event_name(event: HookEvent) -> &'static str {
match event {
HookEvent::BeforeInsert => "before_insert",
HookEvent::AfterInsert => "after_insert",
HookEvent::BeforeUpdate => "before_update",
HookEvent::AfterUpdate => "after_update",
HookEvent::BeforeDelete => "before_delete",
HookEvent::AfterDelete => "after_delete",
HookEvent::BeforeWrite => "before_write",
HookEvent::AfterWrite => "after_write",
HookEvent::BeforeSave => "before_save",
HookEvent::AfterSave => "after_save",
HookEvent::BeforeRestore => "before_restore",
HookEvent::AfterRestore => "after_restore",
HookEvent::BeforeFind => "before_find",
HookEvent::AfterFind => "after_find",
HookEvent::BeforeValidate => "before_validate",
HookEvent::AfterValidate => "after_validate",
}
}
pub fn event_from_name(name: &str) -> Option<HookEvent> {
match name {
"before_insert" => Some(HookEvent::BeforeInsert),
"after_insert" => Some(HookEvent::AfterInsert),
"before_update" => Some(HookEvent::BeforeUpdate),
"after_update" => Some(HookEvent::AfterUpdate),
"before_delete" => Some(HookEvent::BeforeDelete),
"after_delete" => Some(HookEvent::AfterDelete),
"before_write" => Some(HookEvent::BeforeWrite),
"after_write" => Some(HookEvent::AfterWrite),
"before_save" => Some(HookEvent::BeforeSave),
"after_save" => Some(HookEvent::AfterSave),
"before_restore" => Some(HookEvent::BeforeRestore),
"after_restore" => Some(HookEvent::AfterRestore),
"before_find" => Some(HookEvent::BeforeFind),
"after_find" => Some(HookEvent::AfterFind),
"before_validate" => Some(HookEvent::BeforeValidate),
"after_validate" => Some(HookEvent::AfterValidate),
_ => None,
}
}
pub const INSERT_ORDER: [HookEvent; 8] = [
HookEvent::BeforeWrite,
HookEvent::BeforeSave,
HookEvent::BeforeValidate,
HookEvent::AfterValidate,
HookEvent::BeforeInsert,
HookEvent::AfterInsert,
HookEvent::AfterSave,
HookEvent::AfterWrite,
];
pub const UPDATE_ORDER: [HookEvent; 8] = [
HookEvent::BeforeWrite,
HookEvent::BeforeSave,
HookEvent::BeforeValidate,
HookEvent::AfterValidate,
HookEvent::BeforeUpdate,
HookEvent::AfterUpdate,
HookEvent::AfterSave,
HookEvent::AfterWrite,
];
pub const DELETE_ORDER: [HookEvent; 2] = [HookEvent::BeforeDelete, HookEvent::AfterDelete];
pub const RESTORE_ORDER: [HookEvent; 2] = [HookEvent::BeforeRestore, HookEvent::AfterRestore];
pub const FIND_ORDER: [HookEvent; 2] = [HookEvent::BeforeFind, HookEvent::AfterFind];
use std::sync::{Arc, Mutex};
pub struct HookExecutionRecorder {
events: Mutex<Vec<HookEvent>>,
}
impl Default for HookExecutionRecorder {
fn default() -> Self {
Self::new()
}
}
impl HookExecutionRecorder {
pub fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
}
}
pub fn record(&self, event: HookEvent) {
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
pub fn events(&self) -> Vec<HookEvent> {
self.events
.lock()
.map(|events| events.clone())
.unwrap_or_default()
}
pub fn event_names(&self) -> Vec<&'static str> {
self.events
.lock()
.map(|events| events.iter().map(|e| event_name(*e)).collect())
.unwrap_or_default()
}
pub fn clear(&self) {
if let Ok(mut events) = self.events.lock() {
events.clear();
}
}
pub fn len(&self) -> usize {
self.events.lock().map(|e| e.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn assert_order(&self, expected: &[HookEvent]) -> Result<(), String> {
let actual = self.events();
if actual.len() != expected.len() {
return Err(format!(
"事件数量不匹配:expected {} 个 {:?},actual {} 个 {:?}",
expected.len(),
expected.iter().map(|e| event_name(*e)).collect::<Vec<_>>(),
actual.len(),
actual.iter().map(|e| event_name(*e)).collect::<Vec<_>>(),
));
}
for (i, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
if actual != expected {
return Err(format!(
"事件顺序不一致 @ index {}:expected {:?},actual {:?}",
i,
event_name(*expected),
event_name(*actual)
));
}
}
Ok(())
}
}
pub fn validate_insert_order(registry: &HookRegistry) -> Result<(), String> {
let recorder = Arc::new(HookExecutionRecorder::new());
for event in INSERT_ORDER.iter() {
let r = Arc::clone(&recorder);
registry.register(
*event,
Arc::new(move |_ctx| {
r.record(*event);
Ok(())
}),
);
}
let ctx = HookContext::new();
for event in INSERT_ORDER.iter() {
registry
.dispatch(*event, &ctx)
.map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
}
recorder.assert_order(&INSERT_ORDER)
}
pub fn validate_update_order(registry: &HookRegistry) -> Result<(), String> {
let recorder = Arc::new(HookExecutionRecorder::new());
for event in UPDATE_ORDER.iter() {
let r = Arc::clone(&recorder);
registry.register(
*event,
Arc::new(move |_ctx| {
r.record(*event);
Ok(())
}),
);
}
let ctx = HookContext::new();
for event in UPDATE_ORDER.iter() {
registry
.dispatch(*event, &ctx)
.map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
}
recorder.assert_order(&UPDATE_ORDER)
}
pub fn validate_delete_order(registry: &HookRegistry) -> Result<(), String> {
let recorder = Arc::new(HookExecutionRecorder::new());
for event in DELETE_ORDER.iter() {
let r = Arc::clone(&recorder);
registry.register(
*event,
Arc::new(move |_ctx| {
r.record(*event);
Ok(())
}),
);
}
let ctx = HookContext::new();
for event in DELETE_ORDER.iter() {
registry
.dispatch(*event, &ctx)
.map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
}
recorder.assert_order(&DELETE_ORDER)
}
pub fn validate_restore_order(registry: &HookRegistry) -> Result<(), String> {
let recorder = Arc::new(HookExecutionRecorder::new());
for event in RESTORE_ORDER.iter() {
let r = Arc::clone(&recorder);
registry.register(
*event,
Arc::new(move |_ctx| {
r.record(*event);
Ok(())
}),
);
}
let ctx = HookContext::new();
for event in RESTORE_ORDER.iter() {
registry
.dispatch(*event, &ctx)
.map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
}
recorder.assert_order(&RESTORE_ORDER)
}
pub fn validate_find_order(registry: &HookRegistry) -> Result<(), String> {
let recorder = Arc::new(HookExecutionRecorder::new());
for event in FIND_ORDER.iter() {
let r = Arc::clone(&recorder);
registry.register(
*event,
Arc::new(move |_ctx| {
r.record(*event);
Ok(())
}),
);
}
let ctx = HookContext::new();
for event in FIND_ORDER.iter() {
registry
.dispatch(*event, &ctx)
.map_err(|e| format!("dispatch {:?} 失败:{}", event, e))?;
}
recorder.assert_order(&FIND_ORDER)
}
pub trait HookContextExt {
fn with_meta(self, key: impl Into<String>, value: impl Into<String>) -> Self;
fn with_metas<I, K, V>(self, entries: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>;
}
impl HookContextExt for HookContext {
fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
fn with_metas<I, K, V>(mut self, entries: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
for (key, value) in entries {
self.metadata.insert(key.into(), value.into());
}
self
}
}
pub fn hook_context() -> HookContext {
HookContext::new()
}
pub fn hook_context_with_tenant(tenant_id: i64) -> HookContext {
HookContext::new().with_tenant(tenant_id)
}
pub fn hook_context_with_operator(operator_id: i64) -> HookContext {
HookContext::new().with_operator(operator_id)
}
pub fn hook_context_from_meta<I, K, V>(entries: I) -> HookContext
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
HookContext::new().with_metas(entries)
}
pub const DEFAULT_SOFT_DELETE_FIELD: &str = "delete_time";
pub fn soft_delete_filter_sql(field: &str) -> String {
format!("{} IS NULL", field)
}
pub fn only_trashed_filter_sql(field: &str) -> String {
format!("{} IS NOT NULL", field)
}
pub fn is_soft_deleted(field_value: Option<&str>) -> bool {
match field_value {
None => false,
Some(v) => !v.is_empty(),
}
}
const fn is_valid_identifier(name: &str) -> bool {
if name.is_empty() || name.len() > 63 {
return false;
}
let bytes = name.as_bytes();
if !bytes[0].is_ascii_alphabetic() && bytes[0] != b'_' {
return false;
}
let mut i = 1;
while i < bytes.len() {
if !bytes[i].is_ascii_alphanumeric() && bytes[i] != b'_' {
return false;
}
i += 1;
}
true
}
pub fn soft_delete_update_sql(table: &str, field: &str, pk: &str) -> String {
debug_assert!(
is_valid_identifier(table),
"table must be a valid SQL identifier, got {:?}",
table
);
debug_assert!(
is_valid_identifier(field),
"field must be a valid SQL identifier, got {:?}",
field
);
debug_assert!(
is_valid_identifier(pk),
"pk must be a valid SQL identifier, got {:?}",
pk
);
format!("UPDATE {} SET {} = NOW() WHERE {} = ?", table, field, pk)
}
pub fn soft_delete_restore_sql(table: &str, field: &str, pk: &str) -> String {
debug_assert!(
is_valid_identifier(table),
"table must be a valid SQL identifier, got {:?}",
table
);
debug_assert!(
is_valid_identifier(field),
"field must be a valid SQL identifier, got {:?}",
field
);
debug_assert!(
is_valid_identifier(pk),
"pk must be a valid SQL identifier, got {:?}",
pk
);
format!("UPDATE {} SET {} = NULL WHERE {} = ?", table, field, pk)
}
pub const DEFAULT_TENANT_FIELD: &str = "app_id";
pub fn tenant_filter_sql(field: &str, table: &str) -> String {
format!("{}.{} = ?", table, field)
}
pub fn tenant_filter_sql_no_table(field: &str) -> String {
format!("{} = ?", field)
}
pub fn is_tenant_aware(app_id: i64) -> bool {
app_id > 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_events_count() {
assert_eq!(ALL_EVENTS.len(), 16, "16 事件完整列表");
}
#[test]
fn test_php_native_events_count() {
assert_eq!(PHP_NATIVE_EVENTS.len(), 12, "PHP 原生 12 事件");
}
#[test]
fn test_extended_events_count() {
assert_eq!(EXTENDED_EVENTS.len(), 4, "sz-orm-core 扩展 4 事件");
}
#[test]
fn test_all_events_no_duplicates() {
let mut names: Vec<&str> = ALL_EVENTS.iter().map(|e| event_name(*e)).collect();
names.sort();
names.dedup();
assert_eq!(names.len(), ALL_EVENTS.len(), "ALL_EVENTS 不应有重复");
}
#[test]
fn test_native_plus_extended_equals_all() {
let mut all_names: Vec<&str> = ALL_EVENTS.iter().map(|e| event_name(*e)).collect();
all_names.sort();
let mut combined: Vec<&str> = PHP_NATIVE_EVENTS
.iter()
.chain(EXTENDED_EVENTS.iter())
.map(|e| event_name(*e))
.collect();
combined.sort();
assert_eq!(combined, all_names, "PHP_NATIVE + EXTENDED == ALL");
}
#[test]
fn test_event_name_php_style() {
assert_eq!(event_name(HookEvent::BeforeInsert), "before_insert");
assert_eq!(event_name(HookEvent::AfterInsert), "after_insert");
assert_eq!(event_name(HookEvent::BeforeUpdate), "before_update");
assert_eq!(event_name(HookEvent::AfterUpdate), "after_update");
assert_eq!(event_name(HookEvent::BeforeDelete), "before_delete");
assert_eq!(event_name(HookEvent::AfterDelete), "after_delete");
assert_eq!(event_name(HookEvent::BeforeWrite), "before_write");
assert_eq!(event_name(HookEvent::AfterWrite), "after_write");
assert_eq!(event_name(HookEvent::BeforeSave), "before_save");
assert_eq!(event_name(HookEvent::AfterSave), "after_save");
assert_eq!(event_name(HookEvent::BeforeRestore), "before_restore");
assert_eq!(event_name(HookEvent::AfterRestore), "after_restore");
assert_eq!(event_name(HookEvent::BeforeFind), "before_find");
assert_eq!(event_name(HookEvent::AfterFind), "after_find");
assert_eq!(event_name(HookEvent::BeforeValidate), "before_validate");
assert_eq!(event_name(HookEvent::AfterValidate), "after_validate");
}
#[test]
fn test_event_from_name_roundtrip() {
for event in ALL_EVENTS.iter() {
let name = event_name(*event);
let back = event_from_name(name);
assert_eq!(back, Some(*event), "事件 {:?} 往返映射失败", event);
}
}
#[test]
fn test_event_from_name_unknown() {
assert_eq!(event_from_name("unknown_event"), None);
assert_eq!(event_from_name(""), None);
assert_eq!(event_from_name("BeforeInsert"), None, "大小写敏感");
assert_eq!(event_from_name("before-insert"), None, "需 snake_case");
}
#[test]
fn test_insert_order_aligns_php() {
assert_eq!(
INSERT_ORDER,
[
HookEvent::BeforeWrite,
HookEvent::BeforeSave,
HookEvent::BeforeValidate,
HookEvent::AfterValidate,
HookEvent::BeforeInsert,
HookEvent::AfterInsert,
HookEvent::AfterSave,
HookEvent::AfterWrite,
]
);
let php_order = [
HookEvent::BeforeWrite,
HookEvent::BeforeInsert,
HookEvent::AfterInsert,
HookEvent::AfterWrite,
];
let mut php_idx = 0;
for event in INSERT_ORDER.iter() {
if php_idx < php_order.len() && *event == php_order[php_idx] {
php_idx += 1;
}
}
assert_eq!(php_idx, php_order.len(), "PHP 原生顺序应作为子序列保留");
}
#[test]
fn test_update_order_aligns_php() {
assert_eq!(
UPDATE_ORDER,
[
HookEvent::BeforeWrite,
HookEvent::BeforeSave,
HookEvent::BeforeValidate,
HookEvent::AfterValidate,
HookEvent::BeforeUpdate,
HookEvent::AfterUpdate,
HookEvent::AfterSave,
HookEvent::AfterWrite,
]
);
}
#[test]
fn test_delete_order_aligns_php() {
assert_eq!(
DELETE_ORDER,
[HookEvent::BeforeDelete, HookEvent::AfterDelete]
);
}
#[test]
fn test_restore_order_aligns_php() {
assert_eq!(
RESTORE_ORDER,
[HookEvent::BeforeRestore, HookEvent::AfterRestore]
);
}
#[test]
fn test_find_order_aligns_php() {
assert_eq!(FIND_ORDER, [HookEvent::BeforeFind, HookEvent::AfterFind]);
}
#[test]
fn test_recorder_empty() {
let r = HookExecutionRecorder::new();
assert!(r.is_empty());
assert_eq!(r.len(), 0);
assert_eq!(r.events(), Vec::<HookEvent>::new());
assert_eq!(r.event_names(), Vec::<&str>::new());
}
#[test]
fn test_recorder_record_and_read() {
let r = HookExecutionRecorder::new();
r.record(HookEvent::BeforeInsert);
r.record(HookEvent::AfterInsert);
assert_eq!(r.len(), 2);
assert_eq!(
r.events(),
vec![HookEvent::BeforeInsert, HookEvent::AfterInsert]
);
assert_eq!(r.event_names(), vec!["before_insert", "after_insert"]);
}
#[test]
fn test_recorder_clear() {
let r = HookExecutionRecorder::new();
r.record(HookEvent::BeforeInsert);
r.clear();
assert!(r.is_empty());
}
#[test]
fn test_recorder_assert_order_ok() {
let r = HookExecutionRecorder::new();
r.record(HookEvent::BeforeInsert);
r.record(HookEvent::AfterInsert);
let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
assert!(result.is_ok());
}
#[test]
fn test_recorder_assert_order_mismatch_count() {
let r = HookExecutionRecorder::new();
r.record(HookEvent::BeforeInsert);
let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("数量不匹配"));
}
#[test]
fn test_recorder_assert_order_mismatch_value() {
let r = HookExecutionRecorder::new();
r.record(HookEvent::BeforeInsert);
r.record(HookEvent::BeforeUpdate);
let result = r.assert_order(&[HookEvent::BeforeInsert, HookEvent::AfterInsert]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("顺序不一致"));
assert!(err.contains("after_insert"));
}
#[test]
fn test_validate_insert_order() {
let registry = HookRegistry::new();
let result = validate_insert_order(®istry);
assert!(result.is_ok(), "INSERT 顺序验证应通过:{:?}", result);
}
#[test]
fn test_validate_update_order() {
let registry = HookRegistry::new();
let result = validate_update_order(®istry);
assert!(result.is_ok(), "UPDATE 顺序验证应通过:{:?}", result);
}
#[test]
fn test_validate_delete_order() {
let registry = HookRegistry::new();
let result = validate_delete_order(®istry);
assert!(result.is_ok(), "DELETE 顺序验证应通过:{:?}", result);
}
#[test]
fn test_validate_restore_order() {
let registry = HookRegistry::new();
let result = validate_restore_order(®istry);
assert!(result.is_ok(), "RESTORE 顺序验证应通过:{:?}", result);
}
#[test]
fn test_validate_find_order() {
let registry = HookRegistry::new();
let result = validate_find_order(®istry);
assert!(result.is_ok(), "FIND 顺序验证应通过:{:?}", result);
}
#[test]
fn test_all_16_events_registerable_and_dispatchable() {
let registry = HookRegistry::new();
let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
for event in ALL_EVENTS.iter() {
let c = Arc::clone(&counter);
registry.register(
*event,
Arc::new(move |_ctx| {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}),
);
}
let ctx = HookContext::new();
for event in ALL_EVENTS.iter() {
registry.dispatch(*event, &ctx).expect("dispatch 应成功");
}
assert_eq!(
counter.load(std::sync::atomic::Ordering::SeqCst),
16,
"16 事件全部应被触发一次"
);
}
#[test]
fn test_all_16_events_countable() {
let registry = HookRegistry::new();
for event in ALL_EVENTS.iter() {
registry.register(*event, Arc::new(|_ctx| Ok(())));
}
for event in ALL_EVENTS.iter() {
assert_eq!(
registry.count(*event),
1,
"事件 {:?} 应注册 1 个钩子",
event
);
}
}
#[test]
fn test_r5_php_native_12_events_available() {
for event in PHP_NATIVE_EVENTS.iter() {
let name = event_name(*event);
let back = event_from_name(name);
assert_eq!(
back,
Some(*event),
"PHP 原生事件 {:?} 应在 sz-rust 中可用",
name
);
}
}
#[test]
fn test_r5_php_insert_order_preserved() {
let php_native_order = [
HookEvent::BeforeWrite,
HookEvent::BeforeInsert,
HookEvent::AfterInsert,
HookEvent::AfterWrite,
];
let mut php_idx = 0;
for event in INSERT_ORDER.iter() {
if php_idx < php_native_order.len() && *event == php_native_order[php_idx] {
php_idx += 1;
}
}
assert_eq!(
php_idx,
php_native_order.len(),
"PHP 原生 INSERT 顺序应作为 sz-rust 扩展顺序的子序列保留"
);
}
#[test]
fn test_r5_php_update_order_preserved() {
let php_native_order = [
HookEvent::BeforeWrite,
HookEvent::BeforeUpdate,
HookEvent::AfterUpdate,
HookEvent::AfterWrite,
];
let mut php_idx = 0;
for event in UPDATE_ORDER.iter() {
if php_idx < php_native_order.len() && *event == php_native_order[php_idx] {
php_idx += 1;
}
}
assert_eq!(
php_idx,
php_native_order.len(),
"PHP 原生 UPDATE 顺序应作为 sz-rust 扩展顺序的子序列保留"
);
}
#[test]
fn test_r5_php_delete_order_exact() {
assert_eq!(
DELETE_ORDER,
[HookEvent::BeforeDelete, HookEvent::AfterDelete],
"DELETE 顺序应与 PHP 完全一致(无扩展)"
);
}
#[test]
fn test_r5_php_restore_order_exact() {
assert_eq!(
RESTORE_ORDER,
[HookEvent::BeforeRestore, HookEvent::AfterRestore],
"RESTORE 顺序应与 PHP 完全一致(无扩展)"
);
}
#[test]
fn test_r5_php_find_order_exact() {
assert_eq!(
FIND_ORDER,
[HookEvent::BeforeFind, HookEvent::AfterFind],
"FIND 顺序应与 PHP 完全一致(无扩展)"
);
}
#[test]
fn test_r5_php_actual_usage_before_insert_via_registry() {
let registry = HookRegistry::new();
let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
let c = Arc::clone(&called);
registry.register(
HookEvent::BeforeInsert,
Arc::new(move |_ctx| {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}),
);
let ctx = HookContext::new();
registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
assert_eq!(
called.load(std::sync::atomic::Ordering::SeqCst),
1,
"before_insert 钩子应被触发"
);
}
#[test]
fn test_r5_php_business_level_before_insert_convention_documented() {
let registry = HookRegistry::new();
let recorder = Arc::new(HookExecutionRecorder::new());
let r = Arc::clone(&recorder);
registry.register(
HookEvent::BeforeInsert,
Arc::new(move |_ctx| {
r.record(HookEvent::BeforeInsert);
Ok(())
}),
);
let ctx = HookContext::new();
registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
assert_eq!(recorder.events(), vec![HookEvent::BeforeInsert]);
}
#[test]
fn test_hook_context_re_exported() {
let ctx = HookContext::new()
.with_tenant(42)
.with_operator(1)
.with_timestamp(1700000000);
assert_eq!(ctx.tenant_id, Some(42));
assert_eq!(ctx.operator_id, Some(1));
assert_eq!(ctx.timestamp, 1700000000);
}
#[test]
fn test_hook_context_metadata() {
let mut ctx = HookContext::new();
ctx.set_meta("source", "api");
ctx.set_meta("ip", "127.0.0.1");
assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
assert_eq!(ctx.get_meta("missing"), None);
}
#[test]
fn test_hook_event_is_before() {
assert!(HookEvent::BeforeInsert.is_before());
assert!(HookEvent::BeforeWrite.is_before());
assert!(HookEvent::BeforeSave.is_before());
assert!(HookEvent::BeforeValidate.is_before());
assert!(HookEvent::BeforeFind.is_before());
assert!(HookEvent::BeforeRestore.is_before());
assert!(!HookEvent::AfterInsert.is_before());
}
#[test]
fn test_hook_event_is_after() {
assert!(HookEvent::AfterInsert.is_after());
assert!(HookEvent::AfterWrite.is_after());
assert!(HookEvent::AfterSave.is_after());
assert!(HookEvent::AfterValidate.is_after());
assert!(HookEvent::AfterFind.is_after());
assert!(HookEvent::AfterRestore.is_after());
assert!(!HookEvent::BeforeInsert.is_after());
}
#[test]
fn test_hook_event_is_write_level() {
assert!(HookEvent::BeforeWrite.is_write_level());
assert!(HookEvent::AfterWrite.is_write_level());
assert!(HookEvent::BeforeSave.is_write_level());
assert!(HookEvent::AfterSave.is_write_level());
assert!(!HookEvent::BeforeInsert.is_write_level());
assert!(!HookEvent::BeforeFind.is_write_level());
}
#[test]
fn test_hook_event_is_find_level() {
assert!(HookEvent::BeforeFind.is_find_level());
assert!(HookEvent::AfterFind.is_find_level());
assert!(!HookEvent::BeforeInsert.is_find_level());
}
#[test]
fn test_hook_event_is_validate_level() {
assert!(HookEvent::BeforeValidate.is_validate_level());
assert!(HookEvent::AfterValidate.is_validate_level());
assert!(!HookEvent::BeforeInsert.is_validate_level());
}
#[test]
fn test_hook_event_is_fine_grained() {
for event in EXTENDED_EVENTS.iter() {
assert!(event.is_fine_grained(), "扩展事件 {:?} 应为细粒度", event);
}
assert!(!HookEvent::BeforeInsert.is_fine_grained());
assert!(!HookEvent::AfterInsert.is_fine_grained());
assert!(!HookEvent::BeforeUpdate.is_fine_grained());
assert!(!HookEvent::AfterUpdate.is_fine_grained());
assert!(!HookEvent::BeforeDelete.is_fine_grained());
assert!(!HookEvent::AfterDelete.is_fine_grained());
}
#[test]
fn test_hook_registry_short_circuit_on_error() {
use sz_rust_orm_facade::DbError;
let registry = HookRegistry::new();
let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
let c1 = Arc::clone(&called);
registry.register(
HookEvent::BeforeInsert,
Arc::new(move |_ctx| {
c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}),
);
registry.register(
HookEvent::BeforeInsert,
Arc::new(|_ctx| Err(DbError::Hook("second hook failed".into()))),
);
let c3 = Arc::clone(&called);
registry.register(
HookEvent::BeforeInsert,
Arc::new(move |_ctx| {
c3.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}),
);
let ctx = HookContext::new();
let result = registry.dispatch(HookEvent::BeforeInsert, &ctx);
assert!(result.is_err());
assert_eq!(
called.load(std::sync::atomic::Ordering::SeqCst),
1,
"第二个钩子失败后第三个不应执行"
);
}
#[test]
fn test_hook_registry_clear() {
let registry = HookRegistry::new();
registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
assert_eq!(registry.count(HookEvent::BeforeInsert), 1);
registry.clear(HookEvent::BeforeInsert);
assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
}
#[test]
fn test_hook_registry_clear_all() {
let registry = HookRegistry::new();
registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
registry.register(HookEvent::AfterInsert, Arc::new(|_ctx| Ok(())));
registry.register(HookEvent::BeforeUpdate, Arc::new(|_ctx| Ok(())));
registry.clear_all();
assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
assert_eq!(registry.count(HookEvent::AfterInsert), 0);
assert_eq!(registry.count(HookEvent::BeforeUpdate), 0);
}
#[test]
fn test_hook_registry_dispatch_no_hooks() {
let registry = HookRegistry::new();
let ctx = HookContext::new();
assert!(registry.dispatch(HookEvent::BeforeInsert, &ctx).is_ok());
}
#[test]
fn test_scope_registry_enable_disable() {
let registry = ScopeRegistry::new();
assert!(registry.is_enabled("soft_delete"));
assert!(registry.is_enabled("tenant"));
registry.disable("soft_delete");
assert!(!registry.is_enabled("soft_delete"));
assert!(registry.is_enabled("tenant"));
registry.enable("soft_delete");
assert!(registry.is_enabled("soft_delete"));
}
#[test]
fn test_scope_registry_without_scope() {
let registry = ScopeRegistry::new();
assert!(registry.is_enabled("soft_delete"));
let result = registry.without_scope("soft_delete", || {
assert!(!registry.is_enabled("soft_delete"));
42
});
assert_eq!(result, 42);
assert!(registry.is_enabled("soft_delete"));
}
#[test]
fn test_hook_context_builder_tenant_id() {
let ctx = hook_context().with_tenant(42);
assert_eq!(ctx.tenant_id, Some(42));
assert_eq!(ctx.operator_id, None);
assert_eq!(ctx.timestamp, 0);
}
#[test]
fn test_hook_context_builder_operator_id() {
let ctx = hook_context().with_operator(1);
assert_eq!(ctx.tenant_id, None);
assert_eq!(ctx.operator_id, Some(1));
assert_eq!(ctx.timestamp, 0);
}
#[test]
fn test_hook_context_builder_timestamp() {
let ctx = hook_context().with_timestamp(1700000000);
assert_eq!(ctx.tenant_id, None);
assert_eq!(ctx.operator_id, None);
assert_eq!(ctx.timestamp, 1700000000);
}
#[test]
fn test_hook_context_builder_metadata_with_meta() {
let ctx = hook_context()
.with_meta("source", "api")
.with_meta("ip", "127.0.0.1")
.with_meta("trace_id", "abc123");
assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
assert_eq!(ctx.get_meta("trace_id"), Some(&"abc123".to_string()));
assert_eq!(ctx.get_meta("missing"), None);
assert_eq!(ctx.metadata.len(), 3);
}
#[test]
fn test_hook_context_builder_metadata_with_metas_vec() {
let entries = vec![
("source".to_string(), "api".to_string()),
("ip".to_string(), "127.0.0.1".to_string()),
("trace_id".to_string(), "abc123".to_string()),
];
let ctx = hook_context().with_metas(entries);
assert_eq!(ctx.metadata.len(), 3);
assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
assert_eq!(ctx.get_meta("trace_id"), Some(&"abc123".to_string()));
}
#[test]
fn test_hook_context_builder_metadata_with_metas_array() {
let ctx = hook_context().with_metas([("source", "api"), ("ip", "127.0.0.1")]);
assert_eq!(ctx.metadata.len(), 2);
assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
}
#[test]
fn test_hook_context_builder_full_chain() {
let ctx = hook_context()
.with_tenant(42)
.with_operator(1)
.with_timestamp(1700000000)
.with_meta("source", "api")
.with_meta("ip", "127.0.0.1");
assert_eq!(ctx.tenant_id, Some(42));
assert_eq!(ctx.operator_id, Some(1));
assert_eq!(ctx.timestamp, 1700000000);
assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
assert_eq!(ctx.metadata.len(), 2);
}
#[test]
fn test_hook_context_with_meta_overwrite() {
let ctx = hook_context()
.with_meta("source", "api")
.with_meta("source", "web"); assert_eq!(ctx.get_meta("source"), Some(&"web".to_string()));
assert_eq!(ctx.metadata.len(), 1);
}
#[test]
fn test_hook_context_with_metas_empty() {
let ctx = hook_context().with_metas(Vec::<(String, String)>::new());
assert_eq!(ctx.metadata.len(), 0);
assert!(ctx.metadata.is_empty());
}
#[test]
fn test_hook_context_set_meta_vs_with_meta() {
let mut ctx1 = HookContext::new();
ctx1.set_meta("key", "value1");
ctx1.set_meta("key2", "value2");
let ctx2 = hook_context()
.with_meta("key", "value1")
.with_meta("key2", "value2");
assert_eq!(ctx1.metadata, ctx2.metadata);
}
#[test]
fn test_hook_context_convenience_empty() {
let ctx1 = hook_context();
let ctx2 = HookContext::new();
assert_eq!(ctx1.tenant_id, ctx2.tenant_id);
assert_eq!(ctx1.operator_id, ctx2.operator_id);
assert_eq!(ctx1.timestamp, ctx2.timestamp);
assert_eq!(ctx1.metadata, ctx2.metadata);
}
#[test]
fn test_hook_context_convenience_with_tenant() {
let ctx1 = hook_context_with_tenant(42);
let ctx2 = hook_context().with_tenant(42);
assert_eq!(ctx1.tenant_id, Some(42));
assert_eq!(ctx1.tenant_id, ctx2.tenant_id);
}
#[test]
fn test_hook_context_convenience_with_operator() {
let ctx1 = hook_context_with_operator(1);
let ctx2 = hook_context().with_operator(1);
assert_eq!(ctx1.operator_id, Some(1));
assert_eq!(ctx1.operator_id, ctx2.operator_id);
}
#[test]
fn test_hook_context_convenience_from_meta_vec() {
let headers = vec![
("x-trace-id".to_string(), "abc123".to_string()),
("x-source".to_string(), "api".to_string()),
];
let ctx = hook_context_from_meta(headers);
assert_eq!(ctx.get_meta("x-trace-id"), Some(&"abc123".to_string()));
assert_eq!(ctx.get_meta("x-source"), Some(&"api".to_string()));
assert_eq!(ctx.metadata.len(), 2);
}
#[test]
fn test_hook_context_convenience_from_meta_array() {
let ctx = hook_context_from_meta([("k1", "v1"), ("k2", "v2")]);
assert_eq!(ctx.get_meta("k1"), Some(&"v1".to_string()));
assert_eq!(ctx.get_meta("k2"), Some(&"v2".to_string()));
}
#[test]
fn test_hook_context_convenience_from_meta_empty() {
let ctx = hook_context_from_meta(Vec::<(String, String)>::new());
assert!(ctx.metadata.is_empty());
}
#[test]
fn test_r5_php_trigger_context_passing() {
let ctx = hook_context()
.with_operator(1)
.with_timestamp(1700000000)
.with_meta("action", "insert")
.with_meta("model_class", "Worklogs");
assert_eq!(ctx.operator_id, Some(1), "操作人 ID 应可设置");
assert_eq!(ctx.timestamp, 1700000000, "时间戳应可设置");
assert_eq!(
ctx.get_meta("action"),
Some(&"insert".to_string()),
"action 元数据应可设置"
);
assert_eq!(
ctx.get_meta("model_class"),
Some(&"Worklogs".to_string()),
"model_class 元数据应可设置"
);
}
#[test]
fn test_r5_php_auto_fill_timestamp_via_context() {
let now = 1700000000_u64;
let ctx = hook_context().with_timestamp(now);
let create_time = ctx.timestamp;
let update_time = ctx.timestamp;
assert_eq!(create_time, now, "create_time 应从 ctx.timestamp 获取");
assert_eq!(update_time, now, "update_time 应从 ctx.timestamp 获取");
}
#[test]
fn test_r5_php_worklogs_stat_day_via_context_meta() {
let ctx = hook_context()
.with_timestamp(1700000000)
.with_meta("stat_day", "20231114");
assert_eq!(ctx.get_meta("stat_day"), Some(&"20231114".to_string()));
}
#[test]
fn test_r5_php_tenant_context_via_hook_context() {
let ctx = hook_context_with_tenant(42);
assert_eq!(ctx.tenant_id, Some(42), "租户 ID 应可设置");
}
#[test]
fn test_r5_php_operator_context_via_hook_context() {
let ctx = hook_context_with_operator(1);
assert_eq!(ctx.operator_id, Some(1), "操作人 ID 应可设置");
}
#[test]
fn test_r5_php_event_listen_context_via_hook_registry() {
let registry = HookRegistry::new();
let captured_operator_id = Arc::new(std::sync::Mutex::new(None::<i64>));
let c = Arc::clone(&captured_operator_id);
registry.register(
HookEvent::BeforeInsert,
Arc::new(move |ctx| {
*c.lock().unwrap() = ctx.operator_id;
Ok(())
}),
);
let ctx = hook_context_with_operator(42);
registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
assert_eq!(
*captured_operator_id.lock().unwrap(),
Some(42),
"运行时钩子应能读取 ctx.operator_id"
);
}
#[test]
fn test_r5_php_audit_log_metadata_via_hook_context() {
let ctx = hook_context()
.with_operator(1)
.with_meta("ip", "192.168.1.100")
.with_meta("ua", "Mozilla/5.0")
.with_meta("source", "web")
.with_meta("trace_id", "abc-123-def");
assert_eq!(ctx.get_meta("ip"), Some(&"192.168.1.100".to_string()));
assert_eq!(ctx.get_meta("ua"), Some(&"Mozilla/5.0".to_string()));
assert_eq!(ctx.get_meta("source"), Some(&"web".to_string()));
assert_eq!(ctx.get_meta("trace_id"), Some(&"abc-123-def".to_string()));
assert_eq!(ctx.metadata.len(), 4);
}
#[test]
fn test_r5_php_batch_headers_via_hook_context_from_meta() {
let headers = vec![
("x-request-id".to_string(), "req-001".to_string()),
("x-trace-id".to_string(), "trace-001".to_string()),
("x-tenant-id".to_string(), "42".to_string()),
("x-operator-id".to_string(), "1".to_string()),
];
let ctx = hook_context_from_meta(headers);
assert_eq!(ctx.get_meta("x-request-id"), Some(&"req-001".to_string()));
assert_eq!(ctx.get_meta("x-trace-id"), Some(&"trace-001".to_string()));
assert_eq!(ctx.get_meta("x-tenant-id"), Some(&"42".to_string()));
assert_eq!(ctx.get_meta("x-operator-id"), Some(&"1".to_string()));
assert_eq!(ctx.metadata.len(), 4);
}
#[test]
fn test_default_soft_delete_field() {
assert_eq!(DEFAULT_SOFT_DELETE_FIELD, "delete_time");
}
#[test]
fn test_soft_delete_filter_sql_default_field() {
let sql = soft_delete_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
assert_eq!(sql, "delete_time IS NULL");
}
#[test]
fn test_soft_delete_filter_sql_custom_field() {
let sql = soft_delete_filter_sql("deleted_at");
assert_eq!(sql, "deleted_at IS NULL");
}
#[test]
fn test_only_trashed_filter_sql_default_field() {
let sql = only_trashed_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
assert_eq!(sql, "delete_time IS NOT NULL");
}
#[test]
fn test_only_trashed_filter_sql_custom_field() {
let sql = only_trashed_filter_sql("deleted_at");
assert_eq!(sql, "deleted_at IS NOT NULL");
}
#[test]
fn test_is_soft_deleted_null() {
assert!(!is_soft_deleted(None));
}
#[test]
fn test_is_soft_deleted_empty_string() {
assert!(!is_soft_deleted(Some("")));
}
#[test]
fn test_is_soft_deleted_datetime_value() {
assert!(is_soft_deleted(Some("2026-07-21 10:00:00")));
}
#[test]
fn test_is_soft_deleted_timestamp_value() {
assert!(is_soft_deleted(Some("1700000000")));
}
#[test]
fn test_is_soft_deleted_zero_string() {
assert!(is_soft_deleted(Some("0")));
}
#[test]
fn test_soft_delete_update_sql_default() {
let sql = soft_delete_update_sql("users", DEFAULT_SOFT_DELETE_FIELD, "id");
assert_eq!(sql, "UPDATE users SET delete_time = NOW() WHERE id = ?");
}
#[test]
fn test_soft_delete_update_sql_custom() {
let sql = soft_delete_update_sql("orders", "deleted_at", "order_id");
assert_eq!(
sql,
"UPDATE orders SET deleted_at = NOW() WHERE order_id = ?"
);
}
#[test]
fn test_soft_delete_restore_sql_default() {
let sql = soft_delete_restore_sql("users", DEFAULT_SOFT_DELETE_FIELD, "id");
assert_eq!(sql, "UPDATE users SET delete_time = NULL WHERE id = ?");
}
#[test]
fn test_soft_delete_restore_sql_custom() {
let sql = soft_delete_restore_sql("orders", "deleted_at", "order_id");
assert_eq!(
sql,
"UPDATE orders SET deleted_at = NULL WHERE order_id = ?"
);
}
#[test]
fn test_r5_php_soft_delete_default_field_name() {
assert_eq!(DEFAULT_SOFT_DELETE_FIELD, "delete_time");
assert_ne!(DEFAULT_SOFT_DELETE_FIELD, "deleted_at");
}
#[test]
fn test_r5_php_with_no_trashed_default_condition() {
let sz_rust_sql = soft_delete_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
assert_eq!(sz_rust_sql, "delete_time IS NULL");
let _ = std::marker::PhantomData::<SoftDeleteScope>;
}
#[test]
fn test_r5_php_trashed_logic() {
assert!(!is_soft_deleted(None), "NULL → 未软删除");
assert!(!is_soft_deleted(Some("")), "空字符串 → 未软删除");
assert!(
is_soft_deleted(Some("2026-07-21 10:00:00")),
"有值 → 已软删除"
);
}
#[test]
fn test_r5_php_only_trashed_condition() {
let sql = only_trashed_filter_sql(DEFAULT_SOFT_DELETE_FIELD);
assert_eq!(sql, "delete_time IS NOT NULL");
}
#[test]
fn test_r5_php_delete_sql_format() {
let sql = soft_delete_update_sql("users", "delete_time", "id");
assert!(sql.contains("UPDATE users"));
assert!(sql.contains("SET delete_time = NOW()"));
assert!(sql.contains("WHERE id = ?"));
}
#[test]
fn test_r5_php_restore_sql_format() {
let sql = soft_delete_restore_sql("users", "delete_time", "id");
assert!(sql.contains("UPDATE users"));
assert!(sql.contains("SET delete_time = NULL"));
assert!(sql.contains("WHERE id = ?"));
}
#[test]
fn test_r5_php_basemodel_no_soft_delete_trait() {
let _ = std::marker::PhantomData::<SoftDeleteScope>;
}
#[test]
fn test_r5_php_upload_file_disable_soft_delete() {
struct UploadFile; let _ = std::marker::PhantomData::<UploadFile>;
}
#[test]
fn test_default_tenant_field() {
assert_eq!(DEFAULT_TENANT_FIELD, "app_id");
assert_ne!(DEFAULT_TENANT_FIELD, "tenant_id");
}
#[test]
fn test_tenant_filter_sql_default_field() {
let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "users");
assert_eq!(sql, "users.app_id = ?");
}
#[test]
fn test_tenant_filter_sql_custom_field() {
let sql = tenant_filter_sql("tenant_id", "orders");
assert_eq!(sql, "orders.tenant_id = ?");
}
#[test]
fn test_tenant_filter_sql_with_alias() {
let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "u");
assert_eq!(sql, "u.app_id = ?");
}
#[test]
fn test_tenant_filter_sql_no_table_default() {
let sql = tenant_filter_sql_no_table(DEFAULT_TENANT_FIELD);
assert_eq!(sql, "app_id = ?");
}
#[test]
fn test_tenant_filter_sql_no_table_custom() {
let sql = tenant_filter_sql_no_table("tenant_id");
assert_eq!(sql, "tenant_id = ?");
}
#[test]
fn test_is_tenant_aware_zero() {
assert!(!is_tenant_aware(0));
}
#[test]
fn test_is_tenant_aware_negative() {
assert!(!is_tenant_aware(-1));
assert!(!is_tenant_aware(-100));
}
#[test]
fn test_is_tenant_aware_positive() {
assert!(is_tenant_aware(1));
assert!(is_tenant_aware(42));
assert!(is_tenant_aware(10000));
}
#[test]
fn test_is_tenant_aware_max_i64() {
assert!(is_tenant_aware(i64::MAX));
}
#[test]
fn test_r5_php_tenant_field_name_app_id() {
assert_eq!(DEFAULT_TENANT_FIELD, "app_id");
assert_ne!(DEFAULT_TENANT_FIELD, "tenant_id");
}
#[test]
fn test_r5_php_scope_app_id_condition() {
let sql = tenant_filter_sql(DEFAULT_TENANT_FIELD, "sz_user");
assert_eq!(sql, "sz_user.app_id = ?");
assert!(sql.contains(".app_id"));
}
#[test]
fn test_r5_php_app_id_gt_zero_check() {
assert!(!is_tenant_aware(0), "app_id=0 不启用多租户过滤");
assert!(!is_tenant_aware(-1), "app_id=-1 不启用多租户过滤");
assert!(is_tenant_aware(1), "app_id=1 启用多租户过滤");
assert!(is_tenant_aware(42), "app_id=42 启用多租户过滤");
}
#[test]
fn test_r5_php_global_scope_declaration() {
let scope_name = "app_id"; let registry = ScopeRegistry::new();
registry.enable(scope_name);
assert!(registry.is_enabled(scope_name), "app_id 全局作用域已启用");
registry.disable(scope_name);
assert!(!registry.is_enabled(scope_name), "app_id 全局作用域已禁用");
}
#[test]
fn test_r5_php_module_based_app_id_binding() {
let ctx = hook_context_with_tenant(42);
assert_eq!(ctx.tenant_id, Some(42));
let app_id = ctx.tenant_id.unwrap_or(0);
assert!(is_tenant_aware(app_id));
}
#[test]
fn test_r5_php_tenant_scope_vs_sz_orm_core() {
let php_style = tenant_filter_sql_no_table(DEFAULT_TENANT_FIELD);
assert_eq!(php_style, "app_id = ?");
let sz_orm_style = tenant_filter_sql_no_table("tenant_id");
assert_eq!(sz_orm_style, "tenant_id = ?");
assert_ne!(php_style, sz_orm_style);
}
#[test]
fn test_r5_php_tenant_context_passing() {
let ctx1 = hook_context_with_tenant(100);
let ctx2 = hook_context_with_tenant(200);
assert_ne!(ctx1.tenant_id, ctx2.tenant_id);
assert_eq!(ctx1.tenant_id, Some(100));
assert_eq!(ctx2.tenant_id, Some(200));
}
#[test]
fn test_r5_php_no_cross_tenant_query_when_app_id_zero() {
let app_id = 0;
assert!(!is_tenant_aware(app_id));
}
}