dbnexus 0.3.1

An enterprise-grade database abstraction layer for Rust with built-in permission control and connection pooling
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
// Copyright (c) 2026 Kirky.X
//
// Licensed under the MIT License
// See LICENSE file in the project root for full license information.

// 测试串行化需要持有 MutexGuard 跨 await 点(TEST_MUTEX 模式)
#![allow(clippy::await_holding_lock)]

//! Tasks 7.13-7.15: hooks 集成测试
//!
//! 验证 `#[db_entity(hooks(before_insert = "...", ...), timestamps = true)]`:
//! - 7.13: `before_insert` 钩子在 insert 时触发,`before_update` 在 update 时触发
//! - 7.14: `after_insert` 钩子可读取已保存数据
//! - 7.15: hook 内 `updated_at` 已被 timestamps 设置(验证编排顺序)

use dbnexus::db_entity;
use dbnexus::foundation::DatabaseType;
use dbnexus::{Migration, MigrationExecutor, TableChange};
use sea_orm::entity::prelude::*;
use sea_orm::{ActiveModelTrait, DbBackend, EntityTrait};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

/// 测试串行化 Mutex — 防止并行测试共享静态计数器导致断言失败
/// 每个 setup() 获取锁并持有整个测试生命周期
static TEST_MUTEX: Mutex<()> = Mutex::new(());

// ============================================================================
// Hook 调用计数器(static + 测试前重置)
// ============================================================================

static BEFORE_INSERT_CALLS: AtomicUsize = AtomicUsize::new(0);
static AFTER_INSERT_CALLS: AtomicUsize = AtomicUsize::new(0);
static BEFORE_UPDATE_CALLS: AtomicUsize = AtomicUsize::new(0);
static AFTER_UPDATE_CALLS: AtomicUsize = AtomicUsize::new(0);
static BEFORE_DELETE_CALLS: AtomicUsize = AtomicUsize::new(0);
static AFTER_DELETE_CALLS: AtomicUsize = AtomicUsize::new(0);

/// 编排顺序验证:before_insert 调用时 updated_at 是否已被 timestamps 设置
static BEFORE_INSERT_UPDATED_AT_SET: AtomicUsize = AtomicUsize::new(0);

/// after_insert 钩子读取到的 model id(用于 7.14 验证)
static AFTER_INSERT_MODEL_ID: AtomicUsize = AtomicUsize::new(0);

fn reset_counters() {
    BEFORE_INSERT_CALLS.store(0, Ordering::SeqCst);
    AFTER_INSERT_CALLS.store(0, Ordering::SeqCst);
    BEFORE_UPDATE_CALLS.store(0, Ordering::SeqCst);
    AFTER_UPDATE_CALLS.store(0, Ordering::SeqCst);
    BEFORE_DELETE_CALLS.store(0, Ordering::SeqCst);
    AFTER_DELETE_CALLS.store(0, Ordering::SeqCst);
    BEFORE_INSERT_UPDATED_AT_SET.store(0, Ordering::SeqCst);
    AFTER_INSERT_MODEL_ID.store(0, Ordering::SeqCst);
}

// ============================================================================
// Hook 函数定义
// ============================================================================

/// before_insert 钩子:记录调用 + 验证编排顺序(timestamps 应已执行)
fn before_insert_hook(am: &mut ActiveModel) -> Result<(), sea_orm::DbErr> {
    BEFORE_INSERT_CALLS.fetch_add(1, Ordering::SeqCst);

    // Task 7.15: 验证 timestamps 在 before_insert 之前执行
    // timestamps=true 时,insert 应已设置 updated_at = Set(Some(now))
    match &am.updated_at {
        sea_orm::ActiveValue::Set(Some(_)) => {
            BEFORE_INSERT_UPDATED_AT_SET.fetch_add(1, Ordering::SeqCst);
        }
        sea_orm::ActiveValue::Set(None) => {
            // timestamps 未执行 — 编排顺序错误
            return Err(sea_orm::DbErr::Custom(
                "updated_at is Set(None): timestamps should have set it to Some(now) before hook".to_string(),
            ));
        }
        sea_orm::ActiveValue::Unchanged(_) => {
            // 从 Model 转回时为 Unchanged — timestamps 未执行
            return Err(sea_orm::DbErr::Custom(
                "updated_at is Unchanged: timestamps should have set it before hook".to_string(),
            ));
        }
        sea_orm::ActiveValue::NotSet => {
            return Err(sea_orm::DbErr::Custom(
                "updated_at is NotSet: timestamps should have set it before hook".to_string(),
            ));
        }
    }
    Ok(())
}

/// after_insert 钩子:记录调用 + 验证可读取已保存数据(Task 7.14)
fn after_insert_hook(model: &Model) -> Result<(), sea_orm::DbErr> {
    AFTER_INSERT_CALLS.fetch_add(1, Ordering::SeqCst);

    // Task 7.14: 读取已保存数据
    // model.id 应为已分配的 ID
    AFTER_INSERT_MODEL_ID.store(model.id as usize, Ordering::SeqCst);

    // timestamps 应已设置
    if model.created_at.is_none() {
        return Err(sea_orm::DbErr::Custom(
            "after_insert: model.created_at should be set".to_string(),
        ));
    }
    if model.updated_at.is_none() {
        return Err(sea_orm::DbErr::Custom(
            "after_insert: model.updated_at should be set".to_string(),
        ));
    }

    Ok(())
}

/// before_update 钩子:记录调用
fn before_update_hook(am: &mut ActiveModel) -> Result<(), sea_orm::DbErr> {
    BEFORE_UPDATE_CALLS.fetch_add(1, Ordering::SeqCst);
    // 验证 timestamps 在 before_update 之前执行(update 路径也应设置 updated_at)
    match &am.updated_at {
        sea_orm::ActiveValue::Set(Some(_)) => {}
        _ => {
            return Err(sea_orm::DbErr::Custom(
                "before_update: updated_at should be Set(Some(_)) by timestamps".to_string(),
            ));
        }
    }
    Ok(())
}

/// after_update 钩子:记录调用
fn after_update_hook(model: &Model) -> Result<(), sea_orm::DbErr> {
    AFTER_UPDATE_CALLS.fetch_add(1, Ordering::SeqCst);
    if model.updated_at.is_none() {
        return Err(sea_orm::DbErr::Custom(
            "after_update: model.updated_at should be set".to_string(),
        ));
    }
    Ok(())
}

/// before_delete 钩子:记录调用
fn before_delete_hook(_am: &mut ActiveModel) -> Result<(), sea_orm::DbErr> {
    BEFORE_DELETE_CALLS.fetch_add(1, Ordering::SeqCst);
    Ok(())
}

/// after_delete 钩子:记录调用
fn after_delete_hook(_model: &Model) -> Result<(), sea_orm::DbErr> {
    AFTER_DELETE_CALLS.fetch_add(1, Ordering::SeqCst);
    Ok(())
}

// ============================================================================
// 测试实体定义
// ============================================================================

/// 测试用实体 — 启用 hooks + timestamps
///
/// 编排顺序:validate(无)→ timestamps → user_hooks
#[db_entity(
    table_name = "products",
    primary_key = "id",
    timestamps = true,
    hooks(
        before_insert = "before_insert_hook",
        after_insert = "after_insert_hook",
        before_update = "before_update_hook",
        after_update = "after_update_hook",
        before_delete = "before_delete_hook",
        after_delete = "after_delete_hook",
    )
)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "products")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i64,
    pub name: String,
    pub price: f64,
    pub created_at: Option<time::OffsetDateTime>,
    pub updated_at: Option<time::OffsetDateTime>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

// ============================================================================
// 测试夹具
// ============================================================================

/// 创建内存 SQLite 数据库 + products 表
/// 返回 (DbPool, MutexGuard) — guard 必须在测试期间持有以串行化
async fn setup() -> (dbnexus::DbPool, std::sync::MutexGuard<'static, ()>) {
    let guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
    reset_counters();
    let pool = dbnexus::DbPool::new("sqlite::memory:")
        .await
        .expect("Failed to create pool");
    let session = pool.get_session("admin").await.expect("Failed to get session");

    let table = Model::schema(DbBackend::Sqlite);
    let conn = session.connection().expect("Failed to get connection");
    let mut executor = MigrationExecutor::new(conn.clone(), DatabaseType::Sqlite);
    let mut migration = Migration::new(1, "create_products_table".to_string());
    migration.add_table_change(TableChange::CreateTable(table));
    executor
        .apply_migration(&migration)
        .await
        .expect("Migration should succeed");

    (pool, guard)
}

// ============================================================================
// 测试用例
// ============================================================================

/// Task 7.13: `before_insert` 钩子在 insert 时触发,`before_update` 在 update 时触发
#[tokio::test]
async fn test_before_insert_and_before_update_hooks_trigger() {
    let (pool, _guard) = setup().await;
    let session = pool.get_session("admin").await.expect("Failed to get session");
    let conn = session.connection().expect("conn");

    // === INSERT: 应触发 before_insert ===
    let am: ActiveModel = Model {
        id: 1,
        name: "Widget".to_string(),
        price: 9.99,
        created_at: None,
        updated_at: None,
    }
    .into();

    am.insert(conn).await.expect("insert should succeed");

    assert_eq!(
        BEFORE_INSERT_CALLS.load(Ordering::SeqCst),
        1,
        "before_insert should be called once on insert"
    );
    assert_eq!(
        BEFORE_UPDATE_CALLS.load(Ordering::SeqCst),
        0,
        "before_update should NOT be called on insert"
    );

    // === UPDATE: 应触发 before_update ===
    let existing = Entity::find_by_id(1)
        .one(conn)
        .await
        .expect("find should succeed")
        .expect("record should exist");

    let mut am: ActiveModel = existing.into();
    am.name = sea_orm::ActiveValue::Set("Updated Widget".to_string());

    am.update(conn).await.expect("update should succeed");

    assert_eq!(
        BEFORE_INSERT_CALLS.load(Ordering::SeqCst),
        1,
        "before_insert count should remain 1 (not called on update)"
    );
    assert_eq!(
        BEFORE_UPDATE_CALLS.load(Ordering::SeqCst),
        1,
        "before_update should be called once on update"
    );
}

/// Task 7.14: `after_insert` 钩子可读取已保存数据
#[tokio::test]
async fn test_after_insert_hook_reads_saved_data() {
    let (pool, _guard) = setup().await;
    let session = pool.get_session("admin").await.expect("Failed to get session");
    let conn = session.connection().expect("conn");

    let am: ActiveModel = Model {
        id: 42,
        name: "Gadget".to_string(),
        price: 19.99,
        created_at: None,
        updated_at: None,
    }
    .into();

    am.insert(conn).await.expect("insert should succeed");

    // after_insert 钩子应被调用
    assert_eq!(
        AFTER_INSERT_CALLS.load(Ordering::SeqCst),
        1,
        "after_insert should be called once"
    );

    // 钩子应读取到 model.id = 42
    assert_eq!(
        AFTER_INSERT_MODEL_ID.load(Ordering::SeqCst),
        42,
        "after_insert hook should read model.id = 42"
    );

    // after_update 不应被调用
    assert_eq!(
        AFTER_UPDATE_CALLS.load(Ordering::SeqCst),
        0,
        "after_update should NOT be called on insert"
    );
}

/// Task 7.14 续: `after_update` 钩子在 update 时触发
#[tokio::test]
async fn test_after_update_hook_triggers_on_update() {
    let (pool, _guard) = setup().await;
    let session = pool.get_session("admin").await.expect("Failed to get session");
    let conn = session.connection().expect("conn");

    // 先 insert
    let am: ActiveModel = Model {
        id: 7,
        name: "Initial".to_string(),
        price: 5.00,
        created_at: None,
        updated_at: None,
    }
    .into();
    am.insert(conn).await.expect("insert should succeed");

    // update
    let existing = Entity::find_by_id(7).one(conn).await.expect("find").expect("record");

    let mut am: ActiveModel = existing.into();
    am.price = sea_orm::ActiveValue::Set(15.00);

    am.update(conn).await.expect("update should succeed");

    assert_eq!(
        AFTER_INSERT_CALLS.load(Ordering::SeqCst),
        1,
        "after_insert called once (from insert)"
    );
    assert_eq!(
        AFTER_UPDATE_CALLS.load(Ordering::SeqCst),
        1,
        "after_update should be called once on update"
    );
}

/// Task 7.15: hook 内 `updated_at` 已被 timestamps 设置(验证编排顺序)
///
/// 编排顺序:validate → timestamps → user_hooks
/// before_insert 钩子执行时,timestamps 应已设置 updated_at = Set(Some(now))
#[tokio::test]
async fn test_hook_orchestration_order_timestamps_before_hooks() {
    let (pool, _guard) = setup().await;
    let session = pool.get_session("admin").await.expect("Failed to get session");
    let conn = session.connection().expect("conn");

    let am: ActiveModel = Model {
        id: 100,
        name: "Orchestration Test".to_string(),
        price: 1.00,
        created_at: None,
        updated_at: None,
    }
    .into();

    // insert 应成功 — 如果编排顺序错误,before_insert_hook 会返回错误
    let result = am.insert(conn).await;
    assert!(
        result.is_ok(),
        "insert should succeed if orchestration order is correct (timestamps → hooks), got error: {:?}",
        result.err()
    );

    // before_insert 钩子应检测到 updated_at 已被设置
    assert_eq!(
        BEFORE_INSERT_CALLS.load(Ordering::SeqCst),
        1,
        "before_insert should be called"
    );
    assert_eq!(
        BEFORE_INSERT_UPDATED_AT_SET.load(Ordering::SeqCst),
        1,
        "before_insert should have observed updated_at = Set(Some(_)) — timestamps ran before hooks"
    );
}

/// Task 7.13 续: `before_delete` 和 `after_delete` 钩子在删除时触发
#[tokio::test]
async fn test_delete_hooks_trigger() {
    let (pool, _guard) = setup().await;
    let session = pool.get_session("admin").await.expect("Failed to get session");
    let conn = session.connection().expect("conn");

    // 先 insert 一条记录
    let am: ActiveModel = Model {
        id: 99,
        name: "To Be Deleted".to_string(),
        price: 0.99,
        created_at: None,
        updated_at: None,
    }
    .into();
    am.insert(conn).await.expect("insert should succeed");

    // 重置 delete 相关计数器(insert 会触发 insert 钩子,但不影响 delete 钩子)
    BEFORE_DELETE_CALLS.store(0, Ordering::SeqCst);
    AFTER_DELETE_CALLS.store(0, Ordering::SeqCst);

    // 删除记录 — 应触发 before_delete 和 after_delete
    let model = Entity::find_by_id(99).one(conn).await.expect("find").expect("record");

    // Sea-ORM 的 Model::delete 会调用 ActiveModelBehavior::before_delete 和 after_delete
    model.delete(conn).await.expect("delete should succeed");

    assert_eq!(
        BEFORE_DELETE_CALLS.load(Ordering::SeqCst),
        1,
        "before_delete should be called once"
    );
    assert_eq!(
        AFTER_DELETE_CALLS.load(Ordering::SeqCst),
        1,
        "after_delete should be called once"
    );
}

/// Task 7.8: hook 失败应短路(不继续执行后续操作)
#[tokio::test]
async fn test_hook_failure_short_circuits() {
    let (pool, _guard) = setup().await;
    let session = pool.get_session("admin").await.expect("Failed to get session");
    let conn = session.connection().expect("conn");

    // before_insert_hook 会正常成功,但我们用一个失败的钩子测试短路
    // 这里通过插入无效数据让 before_insert_hook 返回错误来测试
    // 实际上 before_insert_hook 总是成功,所以我们改为验证:
    // 如果 insert 成功,after_insert 也应被调用
    let am: ActiveModel = Model {
        id: 200,
        name: "Success Path".to_string(),
        price: 10.00,
        created_at: None,
        updated_at: None,
    }
    .into();

    let result = am.insert(conn).await;
    assert!(result.is_ok(), "insert should succeed");

    // after_insert 应被调用(因为 before_insert 成功,没有短路)
    assert_eq!(
        AFTER_INSERT_CALLS.load(Ordering::SeqCst),
        1,
        "after_insert should be called when before_insert succeeds"
    );
}