Skip to main content

sz_orm_core/
hooks.rs

1//! 钩子系统(Hooks)— 软删除 + 多租户
2//!
3//! 对应设计文档 3.8 节「钩子系统」。
4//!
5//! # 核心概念
6//!
7//! - **HookContext**:钩子执行上下文,包含当前租户、操作人、时间戳等
8//! - **Hookable**:可钩选的 Model,支持 before/after insert/update/delete 6 个生命周期
9//! - **SoftDelete**:软删除 trait,标记删除而非物理删除
10//! - **GlobalScope**:全局查询作用域,自动过滤(如自动排除软删除行、自动追加租户条件)
11//! - **TenantScope**:多租户全局作用域,自动追加 `tenant_id = ?` 条件
12//!
13//! # 使用示例
14//!
15//! ```no_run
16//! use sz_orm_core::hooks::{HookContext, Hookable, SoftDelete, TenantScope, GlobalScope};
17//!
18//! // 1. 定义带软删除+多租户的 Model
19//! // 2. 查询时自动过滤 deleted_at IS NULL AND tenant_id = ?
20//! // 3. 删除时自动 UPDATE SET deleted_at = NOW() 而非 DELETE
21//! ```
22
23use crate::error::DbError;
24use std::collections::HashMap;
25use std::sync::{Arc, RwLock};
26
27// =====================================================================
28// HookContext — 钩子执行上下文
29// =====================================================================
30
31/// 钩子执行上下文
32///
33/// 携带请求级别的元数据,供钩子读取/修改。
34#[derive(Debug, Clone, Default)]
35pub struct HookContext {
36    /// 当前租户 ID(多租户场景)
37    pub tenant_id: Option<i64>,
38    /// 当前操作人 ID
39    pub operator_id: Option<i64>,
40    /// 时间戳(Unix 微秒)
41    pub timestamp: u64,
42    /// 额外元数据
43    pub metadata: HashMap<String, String>,
44}
45
46impl HookContext {
47    /// 创建新的空上下文
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// 设置租户 ID
53    pub fn with_tenant(mut self, tenant_id: i64) -> Self {
54        self.tenant_id = Some(tenant_id);
55        self
56    }
57
58    /// 设置操作人 ID
59    pub fn with_operator(mut self, operator_id: i64) -> Self {
60        self.operator_id = Some(operator_id);
61        self
62    }
63
64    /// 设置时间戳
65    pub fn with_timestamp(mut self, ts: u64) -> Self {
66        self.timestamp = ts;
67        self
68    }
69
70    /// 插入元数据
71    pub fn set_meta(&mut self, key: impl Into<String>, value: impl Into<String>) {
72        self.metadata.insert(key.into(), value.into());
73    }
74
75    /// 读取元数据
76    pub fn get_meta(&self, key: &str) -> Option<&String> {
77        self.metadata.get(key)
78    }
79}
80
81// =====================================================================
82// HookEvent — 钩子事件类型
83// =====================================================================
84
85/// 钩子事件类型
86///
87/// 在 think-orm 风格的 6 个细粒度 insert/update/delete 事件基础上,
88/// 进一步提供 6 个通用写入事件(write/save/restore):
89/// - `BeforeWrite` / `AfterWrite`:任何写入操作(insert/update)前后触发
90/// - `BeforeSave` / `AfterSave`:插入或更新保存前后触发(与 write 等价,命名借用 Rails/ActiveRecord)
91/// - `BeforeRestore` / `AfterRestore`:软删除恢复前后触发
92///
93/// 此外还提供 4 个查询/验证事件:
94/// - `BeforeFind` / `AfterFind`:单行 SELECT 前后触发(可用于查询缓存、审计)
95/// - `BeforeValidate` / `AfterValidate`:数据验证前后触发(写入前的业务规则校验)
96///
97/// 触发顺序示例(执行 INSERT 时):
98/// `BeforeWrite` → `BeforeSave` → `BeforeValidate` → `BeforeInsert` → (INSERT) → `AfterInsert` → `AfterSave` → `AfterWrite`
99///
100/// 触发顺序示例(执行 SELECT 时):
101/// `BeforeFind` → (SELECT) → `AfterFind`
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum HookEvent {
104    /// 插入前触发
105    BeforeInsert,
106    /// 插入后触发
107    AfterInsert,
108    /// 更新前触发
109    BeforeUpdate,
110    /// 更新后触发
111    AfterUpdate,
112    /// 删除前触发
113    BeforeDelete,
114    /// 删除后触发
115    AfterDelete,
116    /// 通用写入前:insert 或 update 前均触发
117    BeforeWrite,
118    /// 通用写入后:insert 或 update 后均触发
119    AfterWrite,
120    /// 保存前(与 BeforeWrite 等价,命名风格不同)
121    BeforeSave,
122    /// 保存后(与 AfterWrite 等价,命名风格不同)
123    AfterSave,
124    /// 软删除恢复前
125    BeforeRestore,
126    /// 软删除恢复后
127    AfterRestore,
128    /// 查询前(单行 SELECT 前触发,可用于查询缓存预热、审计日志)
129    BeforeFind,
130    /// 查询后(单行 SELECT 后触发,可用于查询结果后处理、缓存填充)
131    AfterFind,
132    /// 数据验证前(写入前的业务规则校验,在 before_insert/before_update 之前触发)
133    BeforeValidate,
134    /// 数据验证后(验证完成后触发,可清理临时状态)
135    AfterValidate,
136}
137
138impl HookEvent {
139    /// 是否为 before 事件
140    pub fn is_before(&self) -> bool {
141        matches!(
142            self,
143            HookEvent::BeforeInsert
144                | HookEvent::BeforeUpdate
145                | HookEvent::BeforeDelete
146                | HookEvent::BeforeWrite
147                | HookEvent::BeforeSave
148                | HookEvent::BeforeRestore
149                | HookEvent::BeforeFind
150                | HookEvent::BeforeValidate
151        )
152    }
153
154    /// 是否为 after 事件
155    pub fn is_after(&self) -> bool {
156        matches!(
157            self,
158            HookEvent::AfterInsert
159                | HookEvent::AfterUpdate
160                | HookEvent::AfterDelete
161                | HookEvent::AfterWrite
162                | HookEvent::AfterSave
163                | HookEvent::AfterRestore
164                | HookEvent::AfterFind
165                | HookEvent::AfterValidate
166        )
167    }
168
169    /// 是否为通用写入事件(write/save)
170    pub fn is_write_level(&self) -> bool {
171        matches!(
172            self,
173            HookEvent::BeforeWrite
174                | HookEvent::AfterWrite
175                | HookEvent::BeforeSave
176                | HookEvent::AfterSave
177        )
178    }
179
180    /// 是否为查询事件(find)
181    pub fn is_find_level(&self) -> bool {
182        matches!(self, HookEvent::BeforeFind | HookEvent::AfterFind)
183    }
184
185    /// 是否为验证事件(validate)
186    pub fn is_validate_level(&self) -> bool {
187        matches!(self, HookEvent::BeforeValidate | HookEvent::AfterValidate)
188    }
189
190    /// 是否为细粒度事件(v0.2.0+ 新增的事件)
191    pub fn is_fine_grained(&self) -> bool {
192        self.is_write_level()
193            || self.is_find_level()
194            || self.is_validate_level()
195            || matches!(self, HookEvent::BeforeRestore | HookEvent::AfterRestore)
196    }
197}
198
199// =====================================================================
200// HookResult — 钩子执行结果
201// =====================================================================
202
203/// 钩子执行结果
204pub type HookResult<T> = Result<T, DbError>;
205
206// =====================================================================
207// Hookable — 可钩选 Model trait
208// =====================================================================
209
210/// 可钩选 Model trait
211///
212/// 实现 `Hookable` 的 Model 可以在 insert/update/delete 前后执行自定义逻辑。
213/// 默认实现为 no-op,Model 按需 override。
214///
215/// # 细粒度事件(v0.2.0+)
216///
217/// 在原 6 个 insert/update/delete 钩子之外,新增 6 个通用钩子:
218/// - `before_write` / `after_write`:任何写入(insert 或 update)前后均触发
219/// - `before_save` / `after_save`:保存前后触发(与 write 等价,命名风格不同)
220/// - `before_restore` / `after_restore`:软删除恢复前后触发
221///
222/// 调用方应在执行 INSERT 前依次调用 `before_write` → `before_save` → `before_insert`,
223/// INSERT 完成后依次调用 `after_insert` → `after_save` → `after_write`。
224pub trait Hookable: crate::model::Model {
225    /// 插入前钩子(默认 no-op)
226    fn before_insert(_ctx: &mut HookContext) -> HookResult<()> {
227        Ok(())
228    }
229
230    /// 插入后钩子(默认 no-op)
231    fn after_insert(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
232        Ok(())
233    }
234
235    /// 更新前钩子(默认 no-op)
236    fn before_update(_ctx: &mut HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
237        Ok(())
238    }
239
240    /// 更新后钩子(默认 no-op)
241    fn after_update(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
242        Ok(())
243    }
244
245    /// 删除前钩子(默认 no-op)
246    fn before_delete(_ctx: &mut HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
247        Ok(())
248    }
249
250    /// 删除后钩子(默认 no-op)
251    fn after_delete(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
252        Ok(())
253    }
254
255    /// 通用写入前钩子:insert 或 update 前均触发(默认 no-op)
256    ///
257    /// 适合用于审计日志、统一字段填充(如 updated_at = now())等场景。
258    fn before_write(_ctx: &mut HookContext) -> HookResult<()> {
259        Ok(())
260    }
261
262    /// 通用写入后钩子:insert 或 update 后均触发(默认 no-op)
263    fn after_write(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
264        Ok(())
265    }
266
267    /// 保存前钩子(与 `before_write` 等价,命名风格不同,默认 no-op)
268    fn before_save(_ctx: &mut HookContext) -> HookResult<()> {
269        Ok(())
270    }
271
272    /// 保存后钩子(与 `after_write` 等价,命名风格不同,默认 no-op)
273    fn after_save(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
274        Ok(())
275    }
276
277    /// 软删除恢复前钩子(默认 no-op)
278    ///
279    /// 当软删除行被恢复(`UPDATE deleted_at = NULL`)时触发。
280    fn before_restore(_ctx: &mut HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
281        Ok(())
282    }
283
284    /// 软删除恢复后钩子(默认 no-op)
285    fn after_restore(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
286        Ok(())
287    }
288
289    /// 单行查询前钩子(默认 no-op)
290    ///
291    /// 在执行 `SELECT * FROM ... WHERE pk = ?` 前触发。
292    /// 适合用于查询缓存预热、查询审计日志、强制查询条件注入等。
293    fn before_find(_ctx: &mut HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
294        Ok(())
295    }
296
297    /// 单行查询后钩子(默认 no-op)
298    ///
299    /// 在 `SELECT * FROM ... WHERE pk = ?` 返回结果后触发。
300    /// 适合用于查询结果缓存填充、行级权限校验等。
301    fn after_find(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
302        Ok(())
303    }
304
305    /// 数据验证前钩子(默认 no-op)
306    ///
307    /// 在写入前的业务规则校验之前触发,调用顺序:
308    /// `before_write` → `before_save` → `before_validate` → `validate` → `after_validate` → `before_insert`
309    ///
310    /// 适合用于字段非空校验、字段格式校验、跨字段一致性校验等。
311    /// 失败时返回 `Err(DbError::Validation(...))`,会短路后续 before_insert。
312    fn before_validate(_ctx: &mut HookContext) -> HookResult<()> {
313        Ok(())
314    }
315
316    /// 数据验证逻辑(默认 no-op)
317    ///
318    /// 在 `before_validate` 之后、`after_validate` 之前调用。
319    /// Model 可按需 override 此方法以实现实际的业务规则校验。
320    /// 失败时返回 `Err(DbError::Validation(...))`,会短路后续 `after_validate` 与 `before_insert`。
321    fn validate(_ctx: &mut HookContext) -> HookResult<()> {
322        Ok(())
323    }
324
325    /// 数据验证后钩子(默认 no-op)
326    ///
327    /// 验证成功后触发,可用于清理临时状态、记录验证日志。
328    fn after_validate(_ctx: &HookContext) -> HookResult<()> {
329        Ok(())
330    }
331}
332
333/// 钩子执行辅助工具
334///
335/// 封装常见的钩子触发顺序,避免业务代码手动逐个调用。
336pub struct HookDispatcher;
337
338impl HookDispatcher {
339    /// INSERT 操作的完整钩子序列:
340    /// `before_write` → `before_save` → `before_validate` → `validate` → `after_validate`
341    /// → `before_insert` → (执行) → `after_insert` → `after_save` → `after_write`
342    ///
343    /// `f` 为执行实际 INSERT 操作的闭包,返回插入后的主键。
344    pub fn insert<M, F>(ctx: &mut HookContext, f: F) -> HookResult<M::PrimaryKey>
345    where
346        M: Hookable,
347        F: FnOnce(&mut HookContext) -> HookResult<M::PrimaryKey>,
348    {
349        M::before_write(ctx)?;
350        M::before_save(ctx)?;
351        M::before_validate(ctx)?;
352        M::validate(ctx)?;
353        M::after_validate(ctx)?;
354        M::before_insert(ctx)?;
355        let id = f(ctx)?;
356        M::after_insert(ctx, &id)?;
357        M::after_save(ctx, &id)?;
358        M::after_write(ctx, &id)?;
359        Ok(id)
360    }
361
362    /// UPDATE 操作的完整钩子序列(同 INSERT,含 validate)
363    pub fn update<M, F>(ctx: &mut HookContext, id: &M::PrimaryKey, f: F) -> HookResult<()>
364    where
365        M: Hookable,
366        F: FnOnce(&mut HookContext) -> HookResult<()>,
367    {
368        M::before_write(ctx)?;
369        M::before_save(ctx)?;
370        M::before_validate(ctx)?;
371        M::validate(ctx)?;
372        M::after_validate(ctx)?;
373        M::before_update(ctx, id)?;
374        f(ctx)?;
375        M::after_update(ctx, id)?;
376        M::after_save(ctx, id)?;
377        M::after_write(ctx, id)?;
378        Ok(())
379    }
380
381    /// DELETE 操作的完整钩子序列
382    pub fn delete<M, F>(ctx: &mut HookContext, id: &M::PrimaryKey, f: F) -> HookResult<()>
383    where
384        M: Hookable,
385        F: FnOnce(&mut HookContext) -> HookResult<()>,
386    {
387        M::before_delete(ctx, id)?;
388        f(ctx)?;
389        M::after_delete(ctx, id)?;
390        Ok(())
391    }
392
393    /// RESTORE 操作(软删除恢复)的完整钩子序列
394    pub fn restore<M, F>(ctx: &mut HookContext, id: &M::PrimaryKey, f: F) -> HookResult<()>
395    where
396        M: Hookable,
397        F: FnOnce(&mut HookContext) -> HookResult<()>,
398    {
399        M::before_restore(ctx, id)?;
400        f(ctx)?;
401        M::after_restore(ctx, id)?;
402        Ok(())
403    }
404
405    /// FIND 操作(单行查询)的完整钩子序列:
406    /// `before_find` → (执行 SELECT) → `after_find`
407    ///
408    /// `f` 为执行实际 SELECT 操作的闭包,返回查询到的实例。
409    pub fn find<M, F>(ctx: &mut HookContext, id: &M::PrimaryKey, f: F) -> HookResult<()>
410    where
411        M: Hookable,
412        F: FnOnce(&mut HookContext) -> HookResult<()>,
413    {
414        M::before_find(ctx, id)?;
415        f(ctx)?;
416        M::after_find(ctx, id)?;
417        Ok(())
418    }
419
420    /// 仅触发验证钩子序列(不执行实际写入):
421    /// `before_validate` → `validate` → `after_validate`
422    ///
423    /// 适用于调用方需独立校验数据、不立刻写入的场景。
424    pub fn validate<M>(ctx: &mut HookContext) -> HookResult<()>
425    where
426        M: Hookable,
427    {
428        M::before_validate(ctx)?;
429        M::validate(ctx)?;
430        M::after_validate(ctx)?;
431        Ok(())
432    }
433}
434
435// =====================================================================
436// SoftDelete — 软删除 trait
437// =====================================================================
438
439/// 软删除 trait
440///
441/// 实现此 trait 的 Model 在调用 delete 时,实际执行
442/// `UPDATE SET {field} = NOW() WHERE pk = ?` 而非 `DELETE`。
443pub trait SoftDelete: crate::model::Model {
444    /// 软删除字段名(如 `deleted_at`)
445    fn soft_delete_field() -> &'static str;
446
447    /// 是否已软删除(由 Model 数据行判断)
448    fn is_deleted(&self) -> bool;
449}
450
451// =====================================================================
452// GlobalScope — 全局查询作用域
453// =====================================================================
454
455/// 全局查询作用域
456///
457/// 应用到所有查询的隐式过滤条件。
458/// 典型实现:软删除作用域(`deleted_at IS NULL`)、多租户作用域(`tenant_id = ?`)。
459///
460/// 注:此 trait 不要求实现 `Model`,因为作用域本身只是一个标记类型,
461/// 真正的 Model 由泛型参数 `M` 携带。
462pub trait GlobalScope {
463    /// 作用域名称(用于调试)
464    fn scope_name() -> &'static str;
465
466    /// 返回需要追加的 WHERE 条件 SQL 片段
467    ///
468    /// 返回 `None` 表示无附加条件。
469    /// 返回 `Some((sql, params))` 表示追加 `AND {sql}`,绑定 `params`。
470    fn apply_scope(ctx: &HookContext) -> Option<(String, Vec<crate::value::Value>)>;
471}
472
473// =====================================================================
474// SoftDeleteScope — 软删除全局作用域
475// =====================================================================
476
477/// 软删除全局作用域
478///
479/// 自动追加 `AND {soft_delete_field} IS NULL` 到所有查询。
480/// 需配合 `SoftDelete` trait 使用。
481pub struct SoftDeleteScope;
482
483impl<M: SoftDelete> GlobalScope for (SoftDeleteScope, M) {
484    fn scope_name() -> &'static str {
485        "soft_delete"
486    }
487
488    fn apply_scope(_ctx: &HookContext) -> Option<(String, Vec<crate::value::Value>)> {
489        // 使用完全限定语法避免与 Model::soft_delete_field 歧义
490        let field = <M as SoftDelete>::soft_delete_field();
491        Some((format!("{} IS NULL", field), vec![]))
492    }
493}
494
495// =====================================================================
496// TenantScope — 多租户全局作用域
497// =====================================================================
498
499/// 多租户全局作用域
500///
501/// 自动追加 `AND tenant_id = ?` 到所有查询,绑定 `ctx.tenant_id`。
502/// 若 `ctx.tenant_id` 为 None,则不追加条件(允许跨租户查询,需调用方自行保证安全)。
503pub struct TenantScope;
504
505/// 多租户 Model trait
506///
507/// 实现此 trait 的 Model 自动获得 `TenantScope` 全局作用域。
508pub trait TenantModel: crate::model::Model {
509    /// 租户字段名(默认 `tenant_id`)
510    fn tenant_field() -> &'static str {
511        "tenant_id"
512    }
513
514    /// 获取当前行的租户 ID
515    fn tenant_id(&self) -> i64;
516
517    /// 设置租户 ID
518    fn set_tenant_id(&mut self, tenant_id: i64);
519}
520
521impl<M: TenantModel> GlobalScope for (TenantScope, M) {
522    fn scope_name() -> &'static str {
523        "tenant"
524    }
525
526    fn apply_scope(ctx: &HookContext) -> Option<(String, Vec<crate::value::Value>)> {
527        ctx.tenant_id.map(|tid| {
528            (
529                format!("{} = ?", <M as TenantModel>::tenant_field()),
530                vec![crate::value::Value::I64(tid)],
531            )
532        })
533    }
534}
535
536// =====================================================================
537// HookRegistry — 钩子注册表(运行时钩子)
538// =====================================================================
539
540/// 运行时钩子函数类型
541pub type HookFn = Arc<dyn Fn(&HookContext) -> HookResult<()> + Send + Sync>;
542
543/// 钩子注册表
544///
545/// 支持运行时注册全局钩子函数,按事件类型分组。
546/// 与 `Hookable` trait 互补:trait 用于编译期已知钩子,注册表用于运行时插件。
547pub struct HookRegistry {
548    hooks: RwLock<HashMap<HookEvent, Vec<HookFn>>>,
549}
550
551impl Default for HookRegistry {
552    fn default() -> Self {
553        Self::new()
554    }
555}
556
557impl HookRegistry {
558    /// 创建空注册表
559    pub fn new() -> Self {
560        Self {
561            hooks: RwLock::new(HashMap::new()),
562        }
563    }
564
565    /// 注册钩子
566    pub fn register(&self, event: HookEvent, hook: HookFn) {
567        // lock poisoned 时降级为 no-op,避免级联 panic。
568        if let Ok(mut hooks) = self.hooks.write() {
569            hooks.entry(event).or_default().push(hook);
570        }
571    }
572
573    /// 执行某事件的所有钩子
574    ///
575    /// 按注册顺序执行,任一钩子失败则立即返回错误。
576    pub fn dispatch(&self, event: HookEvent, ctx: &HookContext) -> HookResult<()> {
577        let hooks = match self.hooks.read() {
578            Ok(h) => h,
579            Err(_) => return Ok(()), // lock poisoned → no-op
580        };
581        if let Some(fns) = hooks.get(&event) {
582            for f in fns {
583                f(ctx)?;
584            }
585        }
586        Ok(())
587    }
588
589    /// 清除某事件的所有钩子
590    pub fn clear(&self, event: HookEvent) {
591        if let Ok(mut hooks) = self.hooks.write() {
592            hooks.remove(&event);
593        }
594    }
595
596    /// 清除所有钩子
597    pub fn clear_all(&self) {
598        if let Ok(mut hooks) = self.hooks.write() {
599            hooks.clear();
600        }
601    }
602
603    /// 获取某事件的钩子数量
604    pub fn count(&self, event: HookEvent) -> usize {
605        self.hooks
606            .read()
607            .map(|h| h.get(&event).map(|v| v.len()).unwrap_or(0))
608            .unwrap_or(0)
609    }
610}
611
612// =====================================================================
613// ScopeRegistry — 全局作用域注册表
614// =====================================================================
615
616/// 全局作用域注册表
617///
618/// 管理多个 GlobalScope 的启用/禁用状态。
619/// 典型用法:临时禁用软删除作用域以查询已删除行(`without_scope`)。
620pub struct ScopeRegistry {
621    disabled: RwLock<Vec<String>>,
622}
623
624impl Default for ScopeRegistry {
625    fn default() -> Self {
626        Self::new()
627    }
628}
629
630impl ScopeRegistry {
631    /// 创建空注册表(所有作用域默认启用)
632    pub fn new() -> Self {
633        Self {
634            disabled: RwLock::new(Vec::new()),
635        }
636    }
637
638    /// 禁用指定作用域
639    pub fn disable(&self, scope_name: impl Into<String>) {
640        if let Ok(mut disabled) = self.disabled.write() {
641            let name = scope_name.into();
642            if !disabled.contains(&name) {
643                disabled.push(name);
644            }
645        }
646    }
647
648    /// 启用指定作用域
649    pub fn enable(&self, scope_name: &str) {
650        if let Ok(mut disabled) = self.disabled.write() {
651            disabled.retain(|n| n != scope_name);
652        }
653    }
654
655    /// 检查作用域是否启用
656    pub fn is_enabled(&self, scope_name: &str) -> bool {
657        self.disabled
658            .read()
659            .map(|d| !d.iter().any(|n| n == scope_name))
660            .unwrap_or(true)
661    }
662
663    /// 在闭包内临时禁用作用域
664    ///
665    /// ```no_run
666    /// # use sz_orm_core::hooks::ScopeRegistry;
667    /// let registry = ScopeRegistry::new();
668    /// registry.without_scope("soft_delete", || {
669    ///     // 此处查询会包含已软删除的行
670    /// });
671    /// ```
672    pub fn without_scope<F, R>(&self, scope_name: &str, f: F) -> R
673    where
674        F: FnOnce() -> R,
675    {
676        self.disable(scope_name);
677        let result = f();
678        self.enable(scope_name);
679        result
680    }
681}
682
683// =====================================================================
684// 测试
685// =====================================================================
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690
691    #[test]
692    fn hook_context_builder() {
693        let ctx = HookContext::new()
694            .with_tenant(42)
695            .with_operator(1)
696            .with_timestamp(1700000000);
697
698        assert_eq!(ctx.tenant_id, Some(42));
699        assert_eq!(ctx.operator_id, Some(1));
700        assert_eq!(ctx.timestamp, 1700000000);
701    }
702
703    #[test]
704    fn hook_context_metadata() {
705        let mut ctx = HookContext::new();
706        ctx.set_meta("source", "api");
707        ctx.set_meta("ip", "127.0.0.1");
708
709        assert_eq!(ctx.get_meta("source"), Some(&"api".to_string()));
710        assert_eq!(ctx.get_meta("ip"), Some(&"127.0.0.1".to_string()));
711        assert_eq!(ctx.get_meta("missing"), None);
712    }
713
714    #[test]
715    fn hook_event_is_before_after() {
716        assert!(HookEvent::BeforeInsert.is_before());
717        assert!(!HookEvent::BeforeInsert.is_after());
718        assert!(HookEvent::AfterInsert.is_after());
719        assert!(!HookEvent::AfterInsert.is_before());
720    }
721
722    #[test]
723    fn hook_registry_register_and_dispatch() {
724        let registry = HookRegistry::new();
725        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
726
727        let c = Arc::clone(&counter);
728        registry.register(
729            HookEvent::BeforeInsert,
730            Arc::new(move |_ctx| {
731                c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
732                Ok(())
733            }),
734        );
735
736        let ctx = HookContext::new();
737        registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
738        registry.dispatch(HookEvent::BeforeInsert, &ctx).unwrap();
739
740        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
741    }
742
743    #[test]
744    fn hook_registry_dispatch_no_hooks() {
745        let registry = HookRegistry::new();
746        let ctx = HookContext::new();
747        // 无钩子时 dispatch 应返回 Ok
748        assert!(registry.dispatch(HookEvent::BeforeInsert, &ctx).is_ok());
749    }
750
751    #[test]
752    fn hook_registry_clear() {
753        let registry = HookRegistry::new();
754        registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
755        assert_eq!(registry.count(HookEvent::BeforeInsert), 1);
756
757        registry.clear(HookEvent::BeforeInsert);
758        assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
759    }
760
761    #[test]
762    fn hook_registry_clear_all() {
763        let registry = HookRegistry::new();
764        registry.register(HookEvent::BeforeInsert, Arc::new(|_ctx| Ok(())));
765        registry.register(HookEvent::AfterInsert, Arc::new(|_ctx| Ok(())));
766        registry.register(HookEvent::BeforeUpdate, Arc::new(|_ctx| Ok(())));
767
768        registry.clear_all();
769        assert_eq!(registry.count(HookEvent::BeforeInsert), 0);
770        assert_eq!(registry.count(HookEvent::AfterInsert), 0);
771        assert_eq!(registry.count(HookEvent::BeforeUpdate), 0);
772    }
773
774    #[test]
775    fn scope_registry_enable_disable() {
776        let registry = ScopeRegistry::new();
777
778        assert!(registry.is_enabled("soft_delete"));
779        assert!(registry.is_enabled("tenant"));
780
781        registry.disable("soft_delete");
782        assert!(!registry.is_enabled("soft_delete"));
783        assert!(registry.is_enabled("tenant"));
784
785        registry.enable("soft_delete");
786        assert!(registry.is_enabled("soft_delete"));
787    }
788
789    #[test]
790    fn scope_registry_without_scope() {
791        let registry = ScopeRegistry::new();
792        assert!(registry.is_enabled("soft_delete"));
793
794        let result = registry.without_scope("soft_delete", || {
795            assert!(!registry.is_enabled("soft_delete"));
796            42
797        });
798
799        assert_eq!(result, 42);
800        assert!(registry.is_enabled("soft_delete"));
801    }
802
803    #[test]
804    fn hook_registry_short_circuit_on_error() {
805        let registry = HookRegistry::new();
806        let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
807
808        let c1 = Arc::clone(&called);
809        registry.register(
810            HookEvent::BeforeInsert,
811            Arc::new(move |_ctx| {
812                c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
813                Ok(())
814            }),
815        );
816
817        registry.register(
818            HookEvent::BeforeInsert,
819            Arc::new(|_ctx| Err(DbError::Hook("second hook failed".into()))),
820        );
821
822        let c3 = Arc::clone(&called);
823        registry.register(
824            HookEvent::BeforeInsert,
825            Arc::new(move |_ctx| {
826                c3.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
827                Ok(())
828            }),
829        );
830
831        let ctx = HookContext::new();
832        let result = registry.dispatch(HookEvent::BeforeInsert, &ctx);
833
834        assert!(result.is_err());
835        // 第一个钩子执行,第二个返回错误,第三个不应执行
836        assert_eq!(called.load(std::sync::atomic::Ordering::SeqCst), 1);
837    }
838
839    // ===== 细粒度钩子(v0.2.0+)测试 =====
840
841    #[test]
842    fn hook_event_is_write_level() {
843        assert!(HookEvent::BeforeWrite.is_write_level());
844        assert!(HookEvent::AfterWrite.is_write_level());
845        assert!(HookEvent::BeforeSave.is_write_level());
846        assert!(HookEvent::AfterSave.is_write_level());
847        assert!(!HookEvent::BeforeInsert.is_write_level());
848        assert!(!HookEvent::AfterDelete.is_write_level());
849        assert!(!HookEvent::BeforeRestore.is_write_level());
850    }
851
852    #[test]
853    fn hook_event_before_after_covers_new_variants() {
854        assert!(HookEvent::BeforeWrite.is_before());
855        assert!(HookEvent::BeforeSave.is_before());
856        assert!(HookEvent::BeforeRestore.is_before());
857        assert!(HookEvent::AfterWrite.is_after());
858        assert!(HookEvent::AfterSave.is_after());
859        assert!(HookEvent::AfterRestore.is_after());
860        assert!(!HookEvent::AfterWrite.is_before());
861        assert!(!HookEvent::BeforeWrite.is_after());
862    }
863
864    #[test]
865    fn hook_registry_supports_new_events() {
866        let registry = HookRegistry::new();
867        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
868
869        for event in [
870            HookEvent::BeforeWrite,
871            HookEvent::AfterWrite,
872            HookEvent::BeforeSave,
873            HookEvent::AfterSave,
874            HookEvent::BeforeRestore,
875            HookEvent::AfterRestore,
876        ] {
877            let c = Arc::clone(&counter);
878            registry.register(
879                event,
880                Arc::new(move |_ctx| {
881                    c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
882                    Ok(())
883                }),
884            );
885        }
886
887        let ctx = HookContext::new();
888        for event in [
889            HookEvent::BeforeWrite,
890            HookEvent::AfterWrite,
891            HookEvent::BeforeSave,
892            HookEvent::AfterSave,
893            HookEvent::BeforeRestore,
894            HookEvent::AfterRestore,
895        ] {
896            registry.dispatch(event, &ctx).unwrap();
897        }
898
899        assert_eq!(
900            counter.load(std::sync::atomic::Ordering::SeqCst),
901            6,
902            "所有细粒度事件均应被正确注册与触发"
903        );
904    }
905
906    // ===== HookDispatcher 测试 =====
907
908    struct DispatchTestModel;
909    impl crate::model::Model for DispatchTestModel {
910        type PrimaryKey = i64;
911        fn table_name() -> &'static str {
912            "dispatch_test"
913        }
914        fn pk(&self) -> Self::PrimaryKey {
915            0
916        }
917        fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
918    }
919
920    // 全局计数器:用于在 after_* 钩子中记录调用次数(&HookContext 不可变)
921    static DISPATCH_CALLS: std::sync::OnceLock<Arc<std::sync::atomic::AtomicU32>> =
922        std::sync::OnceLock::new();
923
924    fn dispatch_calls() -> Arc<std::sync::atomic::AtomicU32> {
925        DISPATCH_CALLS
926            .get_or_init(|| Arc::new(std::sync::atomic::AtomicU32::new(0)))
927            .clone()
928    }
929
930    impl Hookable for DispatchTestModel {
931        fn before_write(ctx: &mut HookContext) -> HookResult<()> {
932            ctx.set_meta("before_write", "1");
933            Ok(())
934        }
935        fn before_save(ctx: &mut HookContext) -> HookResult<()> {
936            ctx.set_meta("before_save", "1");
937            Ok(())
938        }
939        fn before_validate(ctx: &mut HookContext) -> HookResult<()> {
940            ctx.set_meta("before_validate", "1");
941            Ok(())
942        }
943        fn after_validate(ctx: &HookContext) -> HookResult<()> {
944            assert_eq!(ctx.get_meta("before_validate"), Some(&"1".to_string()));
945            ctx_set_meta_for_after(ctx, "after_validate", "1");
946            Ok(())
947        }
948        fn before_insert(ctx: &mut HookContext) -> HookResult<()> {
949            ctx.set_meta("before_insert", "1");
950            Ok(())
951        }
952        fn after_insert(ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
953            assert_eq!(ctx.get_meta("before_write"), Some(&"1".to_string()));
954            assert_eq!(ctx.get_meta("before_save"), Some(&"1".to_string()));
955            assert_eq!(ctx.get_meta("before_insert"), Some(&"1".to_string()));
956            assert_eq!(ctx.get_meta("before_validate"), Some(&"1".to_string()));
957            Ok(())
958        }
959        fn after_save(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
960            dispatch_calls().fetch_add(1, std::sync::atomic::Ordering::SeqCst);
961            Ok(())
962        }
963        fn after_write(_ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
964            dispatch_calls().fetch_add(1, std::sync::atomic::Ordering::SeqCst);
965            Ok(())
966        }
967        fn before_find(ctx: &mut HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
968            ctx.set_meta("before_find", "1");
969            Ok(())
970        }
971        fn after_find(ctx: &HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
972            assert_eq!(ctx.get_meta("before_find"), Some(&"1".to_string()));
973            ctx_set_meta_for_after(ctx, "after_find", "1");
974            Ok(())
975        }
976    }
977
978    // after_* 钩子接收 &HookContext(不可变),无法直接 set_meta
979    // 使用 AtomicU32 计数器记录 after_* 调用次数(无锁,无线程安全问题)
980    static AFTER_VALIDATE_COUNT: std::sync::atomic::AtomicU32 =
981        std::sync::atomic::AtomicU32::new(0);
982    static AFTER_FIND_COUNT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
983
984    fn ctx_set_meta_for_after(_ctx: &HookContext, key: &str, _value: &str) {
985        match key {
986            "after_validate" => {
987                AFTER_VALIDATE_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
988            }
989            "after_find" => {
990                AFTER_FIND_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
991            }
992            _ => {}
993        }
994    }
995
996    fn after_call_was(key: &str) -> bool {
997        match key {
998            "after_validate" => AFTER_VALIDATE_COUNT.load(std::sync::atomic::Ordering::SeqCst) > 0,
999            "after_find" => AFTER_FIND_COUNT.load(std::sync::atomic::Ordering::SeqCst) > 0,
1000            _ => false,
1001        }
1002    }
1003
1004    fn reset_after_calls() {
1005        AFTER_VALIDATE_COUNT.store(0, std::sync::atomic::Ordering::SeqCst);
1006        AFTER_FIND_COUNT.store(0, std::sync::atomic::Ordering::SeqCst);
1007    }
1008
1009    // 串行化锁:全局静态计数器是共享的,并行测试会互相干扰
1010    static HOOK_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1011
1012    #[test]
1013    fn hook_dispatcher_insert_full_sequence() {
1014        let _guard = HOOK_TEST_LOCK.lock().unwrap();
1015        dispatch_calls().store(0, std::sync::atomic::Ordering::SeqCst);
1016        reset_after_calls();
1017        let mut ctx = HookContext::new();
1018        let id = HookDispatcher::insert::<DispatchTestModel, _>(&mut ctx, |_ctx| Ok(42_i64));
1019        assert!(id.is_ok());
1020        assert_eq!(id.unwrap(), 42);
1021        // 验证 before 钩子都已执行
1022        assert_eq!(ctx.get_meta("before_write"), Some(&"1".to_string()));
1023        assert_eq!(ctx.get_meta("before_save"), Some(&"1".to_string()));
1024        assert_eq!(ctx.get_meta("before_insert"), Some(&"1".to_string()));
1025        // 验证 before_validate + after_validate 都已执行
1026        assert_eq!(ctx.get_meta("before_validate"), Some(&"1".to_string()));
1027        assert!(after_call_was("after_validate"));
1028        // 验证 after_save + after_write 都已执行
1029        assert_eq!(
1030            dispatch_calls().load(std::sync::atomic::Ordering::SeqCst),
1031            2
1032        );
1033    }
1034
1035    #[test]
1036    fn hook_dispatcher_insert_short_circuit_on_before_write_error() {
1037        struct ErrorModel;
1038        impl crate::model::Model for ErrorModel {
1039            type PrimaryKey = i64;
1040            fn table_name() -> &'static str {
1041                "error_model"
1042            }
1043            fn pk(&self) -> Self::PrimaryKey {
1044                0
1045            }
1046            fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
1047        }
1048        impl Hookable for ErrorModel {
1049            fn before_write(_ctx: &mut HookContext) -> HookResult<()> {
1050                Err(DbError::Hook("before_write failed".into()))
1051            }
1052        }
1053
1054        let mut ctx = HookContext::new();
1055        let result = HookDispatcher::insert::<ErrorModel, _>(&mut ctx, |_ctx| Ok(1_i64));
1056        assert!(result.is_err());
1057        // before_write 失败,不应执行实际操作
1058    }
1059
1060    #[test]
1061    fn hook_dispatcher_insert_short_circuit_on_before_validate_error() {
1062        struct ValidationFailModel;
1063        impl crate::model::Model for ValidationFailModel {
1064            type PrimaryKey = i64;
1065            fn table_name() -> &'static str {
1066                "validation_fail"
1067            }
1068            fn pk(&self) -> Self::PrimaryKey {
1069                0
1070            }
1071            fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
1072        }
1073        impl Hookable for ValidationFailModel {
1074            fn before_validate(_ctx: &mut HookContext) -> HookResult<()> {
1075                Err(DbError::Validation("name is required".into()))
1076            }
1077        }
1078
1079        let mut ctx = HookContext::new();
1080        let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1081        let c = Arc::clone(&called);
1082        let result = HookDispatcher::insert::<ValidationFailModel, _>(&mut ctx, move |_ctx| {
1083            c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1084            Ok(1_i64)
1085        });
1086        assert!(result.is_err());
1087        // before_validate 失败,实际 INSERT 不应执行
1088        assert_eq!(
1089            called.load(std::sync::atomic::Ordering::SeqCst),
1090            0,
1091            "before_validate 失败应短路 INSERT 操作"
1092        );
1093        // 错误类型应是 Validation
1094        match result.unwrap_err() {
1095            DbError::Validation(msg) => assert_eq!(msg, "name is required"),
1096            other => panic!("期望 Validation 错误,得到 {:?}", other),
1097        }
1098    }
1099
1100    #[test]
1101    fn hook_dispatcher_update_full_sequence() {
1102        let _guard = HOOK_TEST_LOCK.lock().unwrap();
1103        dispatch_calls().store(0, std::sync::atomic::Ordering::SeqCst);
1104        reset_after_calls();
1105        let mut ctx = HookContext::new();
1106        let result =
1107            HookDispatcher::update::<DispatchTestModel, _>(&mut ctx, &42_i64, |_ctx| Ok(()));
1108        assert!(result.is_ok());
1109        // update 也会触发 after_save + after_write
1110        assert_eq!(
1111            dispatch_calls().load(std::sync::atomic::Ordering::SeqCst),
1112            2
1113        );
1114        // update 也会触发 validate
1115        assert_eq!(ctx.get_meta("before_validate"), Some(&"1".to_string()));
1116        assert!(after_call_was("after_validate"));
1117    }
1118
1119    #[test]
1120    fn hook_dispatcher_delete_full_sequence() {
1121        let mut ctx = HookContext::new();
1122        let result =
1123            HookDispatcher::delete::<DispatchTestModel, _>(&mut ctx, &42_i64, |_ctx| Ok(()));
1124        assert!(result.is_ok());
1125    }
1126
1127    #[test]
1128    fn hook_dispatcher_restore_full_sequence() {
1129        let mut ctx = HookContext::new();
1130        let result =
1131            HookDispatcher::restore::<DispatchTestModel, _>(&mut ctx, &42_i64, |_ctx| Ok(()));
1132        assert!(result.is_ok());
1133    }
1134
1135    #[test]
1136    fn hook_dispatcher_find_full_sequence() {
1137        let _guard = HOOK_TEST_LOCK.lock().unwrap();
1138        dispatch_calls().store(0, std::sync::atomic::Ordering::SeqCst);
1139        reset_after_calls();
1140        let mut ctx = HookContext::new();
1141        let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1142        let c = Arc::clone(&called);
1143        let result = HookDispatcher::find::<DispatchTestModel, _>(&mut ctx, &42_i64, move |_ctx| {
1144            c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1145            Ok(())
1146        });
1147        assert!(result.is_ok());
1148        assert_eq!(
1149            called.load(std::sync::atomic::Ordering::SeqCst),
1150            1,
1151            "SELECT 操作应执行一次"
1152        );
1153        // 验证 before_find + after_find 都已执行
1154        assert_eq!(ctx.get_meta("before_find"), Some(&"1".to_string()));
1155        assert!(after_call_was("after_find"));
1156    }
1157
1158    #[test]
1159    fn hook_dispatcher_find_short_circuit_on_before_find_error() {
1160        struct FindFailModel;
1161        impl crate::model::Model for FindFailModel {
1162            type PrimaryKey = i64;
1163            fn table_name() -> &'static str {
1164                "find_fail"
1165            }
1166            fn pk(&self) -> Self::PrimaryKey {
1167                0
1168            }
1169            fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
1170        }
1171        impl Hookable for FindFailModel {
1172            fn before_find(_ctx: &mut HookContext, _id: &Self::PrimaryKey) -> HookResult<()> {
1173                Err(DbError::Hook("before_find blocked".into()))
1174            }
1175        }
1176
1177        let mut ctx = HookContext::new();
1178        let called = Arc::new(std::sync::atomic::AtomicU32::new(0));
1179        let c = Arc::clone(&called);
1180        let result = HookDispatcher::find::<FindFailModel, _>(&mut ctx, &1_i64, move |_ctx| {
1181            c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1182            Ok(())
1183        });
1184        assert!(result.is_err());
1185        assert_eq!(
1186            called.load(std::sync::atomic::Ordering::SeqCst),
1187            0,
1188            "before_find 失败应短路 SELECT"
1189        );
1190    }
1191
1192    #[test]
1193    fn hook_dispatcher_validate_standalone() {
1194        reset_after_calls();
1195        let mut ctx = HookContext::new();
1196        let result = HookDispatcher::validate::<DispatchTestModel>(&mut ctx);
1197        assert!(result.is_ok());
1198        assert_eq!(ctx.get_meta("before_validate"), Some(&"1".to_string()));
1199        assert!(after_call_was("after_validate"));
1200    }
1201
1202    #[test]
1203    fn hook_event_is_find_level_and_is_validate_level() {
1204        assert!(HookEvent::BeforeFind.is_find_level());
1205        assert!(HookEvent::AfterFind.is_find_level());
1206        assert!(HookEvent::BeforeValidate.is_validate_level());
1207        assert!(HookEvent::AfterValidate.is_validate_level());
1208        assert!(!HookEvent::BeforeInsert.is_find_level());
1209        assert!(!HookEvent::BeforeInsert.is_validate_level());
1210        assert!(!HookEvent::BeforeWrite.is_find_level());
1211        assert!(!HookEvent::BeforeWrite.is_validate_level());
1212    }
1213
1214    #[test]
1215    fn hook_event_is_fine_grained_covers_all_v02_events() {
1216        // v0.2.0+ 新增的事件均应被识别为细粒度
1217        assert!(HookEvent::BeforeWrite.is_fine_grained());
1218        assert!(HookEvent::AfterWrite.is_fine_grained());
1219        assert!(HookEvent::BeforeSave.is_fine_grained());
1220        assert!(HookEvent::AfterSave.is_fine_grained());
1221        assert!(HookEvent::BeforeRestore.is_fine_grained());
1222        assert!(HookEvent::AfterRestore.is_fine_grained());
1223        assert!(HookEvent::BeforeFind.is_fine_grained());
1224        assert!(HookEvent::AfterFind.is_fine_grained());
1225        assert!(HookEvent::BeforeValidate.is_fine_grained());
1226        assert!(HookEvent::AfterValidate.is_fine_grained());
1227        // 原生 6 事件不应标记为细粒度
1228        assert!(!HookEvent::BeforeInsert.is_fine_grained());
1229        assert!(!HookEvent::AfterInsert.is_fine_grained());
1230        assert!(!HookEvent::BeforeUpdate.is_fine_grained());
1231        assert!(!HookEvent::AfterUpdate.is_fine_grained());
1232        assert!(!HookEvent::BeforeDelete.is_fine_grained());
1233        assert!(!HookEvent::AfterDelete.is_fine_grained());
1234    }
1235
1236    #[test]
1237    fn hook_registry_supports_find_and_validate_events() {
1238        let registry = HookRegistry::new();
1239        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
1240
1241        for event in [
1242            HookEvent::BeforeFind,
1243            HookEvent::AfterFind,
1244            HookEvent::BeforeValidate,
1245            HookEvent::AfterValidate,
1246        ] {
1247            let c = Arc::clone(&counter);
1248            registry.register(
1249                event,
1250                Arc::new(move |_ctx| {
1251                    c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1252                    Ok(())
1253                }),
1254            );
1255        }
1256
1257        let ctx = HookContext::new();
1258        for event in [
1259            HookEvent::BeforeFind,
1260            HookEvent::AfterFind,
1261            HookEvent::BeforeValidate,
1262            HookEvent::AfterValidate,
1263        ] {
1264            registry.dispatch(event, &ctx).unwrap();
1265        }
1266
1267        assert_eq!(
1268            counter.load(std::sync::atomic::Ordering::SeqCst),
1269            4,
1270            "find/validate 钩子应能被注册与触发"
1271        );
1272    }
1273
1274    #[test]
1275    fn db_error_validation_error_code_and_display() {
1276        let err = DbError::Validation("name required".into());
1277        assert_eq!(err.error_code(), "DB021");
1278        assert_eq!(format!("{}", err), "Validation error: name required");
1279    }
1280}