Skip to main content

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