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