---
phase: 07-apiergonomics
plan: 03
type: execute
wave: 2
depends_on:
  - 07-01
files_modified:
  - src/sqllog.rs
  - src/lib.rs
  - tests/parser_from_sqllog.rs
autonomous: true
requirements:
  - API-03
  - API-04
tags:
  - rust
  - field-access
  - trait
must_haves:
  truths:
    - "用户可以直接调用 sqllog.exec_time() 获取 u64 执行时间，无需手动解构 parse_performance_metrics 的元组"
    - "sqllog.row_count() 返回 u64 行数"
    - "exec_time() 返回 Result<Option<u64>, ParseError>（Ok(Some(v)) / Ok(None) / Err）"
    - "row_count() 返回 Result<Option<u64>, ParseError>（同上语义）"
    - "FromSqllog trait 定义存在，用户可以 impl FromSqllog for MyType 将 Sqllog 映射到自定义类型"
    - "用户可以通过 .map(MyType::from_sqllog) 在迭代器链中使用"
  artifacts:
    - path: "src/sqllog.rs"
      provides: "exec_time() + row_count() 方法定义，FromSqllog trait 定义"
      contains: "pub fn exec_time" in impl Sqllog
    - path: "src/lib.rs"
      provides: "FromSqllog trait 公开导出"
      contains: "FromSqllog"
    - path: "tests/parser_from_sqllog.rs"
      provides: "exec_time/row_count 测试 + FromSqllog 测试"
      contains: "test_exec_time_returns_value"
  key_links:
    - from: "Sqllog::exec_time"
      to: "Sqllog::parse_indicators"
      via: "内部调用 parse_indicators() 提取 exectime"
      pattern: "parse_indicators"
    - from: "Sqllog::row_count"
      to: "Sqllog::parse_indicators"
      via: "内部调用 parse_indicators() 提取 rowcount"
      pattern: "parse_indicators"
    - from: "tests/parser_from_sqllog.rs"
      to: "FromSqllog trait"
      via: "用户类型实现 FromSqllog"
      pattern: "impl FromSqllog"
    - from: "src/lib.rs pub use"
      to: "FromSqllog"
      via: "re-export"
      pattern: "FromSqllog"
---

<objective>
在 `Sqllog` 上添加 `exec_time()` 和 `row_count()` 直接字段访问方法（API-03），定义 `FromSqllog` trait 并公开导出（API-04）。字段访问方法内部复用 `parse_indicators()` 逻辑，不重复实现解析。

Purpose: API-03（直接字段访问）和 API-04（FromSqllog trait）要求。决策 D-07（返回 Result<Option<u64>>）、D-08（复用 parse_indicators）、D-09（trait 定义）、D-10（.map 组合）的实现。
Output: exec_time()、row_count()、FromSqllog trait、对应测试。
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/STATE.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/phases/07-apiergonomics/07-CONTEXT.md
@CLAUDE.md
@src/sqllog.rs
@src/lib.rs
@tests/performance_metrics.rs

<interfaces>
Key types the executor needs:

```rust
// src/sqllog.rs — 现有 Sqllog 字段
pub struct Sqllog<'a> {
    pub ts: Cow<'a, str>,
    pub meta_raw: Cow<'a, str>,
    pub content_raw: Cow<'a, [u8]>,
    pub tag: Option<Cow<'a, str>>,
    pub(crate) encoding: FileEncodingHint,
}

// 现有方法:
impl<'a> Sqllog<'a> {
    pub fn parse_indicators(&self) -> Option<PerformanceMetrics<'static>> { ... }
    // 返回 Option<PerformanceMetrics>:
    //   None - 无 indicators 块（记录不以 '.' 或 ')' 结尾）
    //   Some(PerformanceMetrics { exectime, rowcount, exec_id, sql })
    // 当 indicators 块存在但不含某个特定字段时，该字段 field 为默认值 0
}

// PerformanceMetrics 的字段类型
pub struct PerformanceMetrics<'a> {
    pub exectime: f32,     // 执行时间（毫秒），默认 0.0
    pub rowcount: u32,     // 行数，默认 0
    pub exec_id: i64,
    pub sql: Cow<'a, str>,
}

// src/lib.rs 当前导出
pub use sqllog::{MetaParts, PerformanceMetrics, Sqllog};
// 需要新增 FromSqllog 导出
```

exec_time() 和 row_count() 的语义约定（D-07）：
- `Ok(Some(v))` — 字段存在且解析成功（exectime > 0.0 表示 EXECTIME 存在）
- `Ok(None)` — 字段不存在（parse_indicators() 返回 None，或返回的 Some 中对应字段为默认值 0）
- `Err(e)` — 字段存在但解析失败（现路径中 parse_indicators 内部忽略解析错误，故 Err 当前未实际使用；为 API 契约保留签名）

内部实现按 D-08 复用 parse_indicators()。

FromSqllog trait 定义按 D-09：
```rust
pub trait FromSqllog {
    fn from_sqllog(s: Sqllog<'_>) -> Self;
}
```
与标准库 From trait 风格一致。用户实现后将 Sqllog 映射到自定义类型。按 D-10，可通过 `.map(MyType::from_sqllog)` 在迭代器链中使用。
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add exec_time() and row_count() to Sqllog</name>
  <files>src/sqllog.rs</files>
  <read_first>
    - src/sqllog.rs (impl Sqllog 块范围，查看现有方法如 body()、parse_indicators()、parse_performance_metrics() 的位置和 doc comment 风格。特别注意 parse_indicators() 返回类型 `Option<PerformanceMetrics<'static>>` 和内部 exectime 字段的类型 f32。)
    - .planning/phases/07-apiergonomics/07-CONTEXT.md (D-07: 返回类型签名 Result<Option<u64>>；D-08: 复用 parse_indicators)
  </read_first>
  <action>
    在 `src/sqllog.rs` 的 `impl<'a> Sqllog<'a>` 块中添加 `exec_time()` 和 `row_count()` 方法。位置：放在 `parse_performance_metrics()` 方法之后、`parse_meta()` 方法之前（在现有公开方法之间，靠近 indicators 相关方法）。

    **A. exec_time() 方法：**
    ```rust
    /// 获取 EXECTIME 字段的值（毫秒）。
    ///
    /// 返回 `Result<Option<u64>, ParseError>`：
    /// - `Ok(Some(v))` — EXECTIME 存在且解析成功
    /// - `Ok(None)` — 记录不含 EXECTIME 字段（infalible path）
    /// - `Err(e)` — 解析失败（当前实现中不会触发，为 API 契约保留）
    ///
    /// 内部复用 `parse_indicators()` 逻辑。
    /// 如需精确的 f32 值，请使用 `parse_performance_metrics()`。
    pub fn exec_time(&self) -> Result<Option<u64>, ParseError> {
        Ok(match self.parse_indicators() {
            None => None,
            Some(m) if m.exectime > 0.0 => Some(m.exectime as u64),
            Some(_) => None,
        })
    }
    ```

    **B. row_count() 方法：**
    ```rust
    /// 获取 ROWCOUNT 字段的值（行数）。
    ///
    /// 返回 `Result<Option<u64>, ParseError>`：
    /// - `Ok(Some(v))` — ROWCOUNT 存在且解析成功
    /// - `Ok(None)` — 记录不含 ROWCOUNT 字段
    /// - `Err(e)` — 解析失败（当前实现中不会触发）
    ///
    /// 内部复用 `parse_indicators()` 逻辑。
    pub fn row_count(&self) -> Result<Option<u64>, ParseError> {
        Ok(match self.parse_indicators() {
            None => None,
            Some(m) if m.rowcount > 0 => Some(m.rowcount as u64),
            Some(_) => None,
        })
    }
    ```

    注意：
    - 两个方法内部均调用 `self.parse_indicators()`，不重复解析逻辑（D-08）
    - 返回类型中的 `ParseError` 使用 `Result` 是为了满足 D-07 的 API 契约。当前实现路径不产生 Err，但预留了未来细化解析错误的可能性
    - 两个方法都需要 `use crate::error::ParseError;` — 检查文件顶部是否已有此导入（sqllog.rs 当前是否 use ParseError？如果否，需要添加）
    - `as u64` 转换：`exectime: f32 as u64` 截断小数部分；`rowcount: u32 as u64` 无损失
    - doc comment 需要说明 `None` 的语义（字段不存在）和 Err 的预留性质
  </action>
  <verify>
    <automated>cargo build 2>&1 | grep -c "^error" | xargs test 0 -eq</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "pub fn exec_time" src/sqllog.rs` 输出为 1
    - `grep -c "pub fn row_count" src/sqllog.rs` 输出为 1
    - `grep "parse_indicators" src/sqllog.rs | grep "exec_time\\|row_count"` 至少 2 行（两个方法都调用 parse_indicators）
    - `grep "m.exectime" src/sqllog.rs` 至少 1 行
    - `grep "m.rowcount" src/sqllog.rs` 至少 1 行
    - 两个方法的返回类型均为 `Result<Option<u64>, ParseError>`
    - `cargo build` 退出码 0
  </acceptance_criteria>
  <done>
    Sqllog 上新增 exec_time() 和 row_count() 方法，返回 Result<Option<u64>, ParseError>，内部复用 parse_indicators()。编译通过。
  </done>
</task>

<task type="auto">
  <name>Task 2: Define FromSqllog trait + lib.rs exports + tests</name>
  <files>src/sqllog.rs, src/lib.rs, tests/parser_from_sqllog.rs</files>
  <read_first>
    - src/sqllog.rs (文件末尾，在 PerformanceMetrics 结构体定义之后，关闭模块的最后一个 pub 类型位置。确定 trait 定义放置位置)
    - src/lib.rs (pub use 列表 lines 73-75)
    - tests/performance_metrics.rs (parse_record 构建方式，build_record 辅助函数模式)
    - .planning/phases/07-apiergonomics/07-CONTEXT.md (D-09: trait 定义 fn from_sqllog(s: Sqllog<'_>) -> Self；D-10: .map 组合使用)
  </read_first>
  <action>
    在 `src/sqllog.rs` 中定义 `FromSqllog` trait。更新 `src/lib.rs` 导出 trait。创建 `tests/parser_from_sqllog.rs` 测试文件。

    **A. 在 src/sqllog.rs 中定义 FromSqllog trait（放在 PerformanceMetrics 结构体定义之后，文件末尾处）：**

    ```rust
    /// 从 `Sqllog` 映射到自定义类型的 trait。
    ///
    /// 类似标准库的 `From` trait，但专用于 `Sqllog` → 目标类型的转换。
    /// 用户实现此 trait 后，可通过 `.map(MyType::from_sqllog)` 在迭代器链中组合使用。
    ///
    /// # Example
    /// ```rust
    /// use dm_database_parser_sqllog::{FromSqllog, Sqllog};
    ///
    /// struct MyRecord {
    ///     timestamp: String,
    ///     sql: String,
    /// }
    ///
    /// impl<'a> FromSqllog for MyRecord {
    ///     fn from_sqllog(s: Sqllog<'a>) -> Self {
    ///         MyRecord {
    ///             timestamp: s.ts.to_string(),
    ///             sql: s.body().to_string(),
    ///         }
    ///     }
    /// }
    /// ```
    pub trait FromSqllog {
        /// 从 `Sqllog` 消费方式创建 `Self`。
        ///
        /// `sqllog` 按值传入；实现者可以获得字段所有权。
        fn from_sqllog(s: Sqllog<'_>) -> Self;
    }
    ```

    **B. 更新 src/lib.rs pub use：**
    在 sqllog 的 pub use 行中添加 `FromSqllog`：
    ```rust
    pub use sqllog::{MetaParts, PerformanceMetrics, Sqllog, FromSqllog};
    ```

    **C. 创建 tests/parser_from_sqllog.rs 测试文件：**

    测试结构：
    ```rust
    use dm_database_parser_sqllog::{LogParserBuilder, Sqllog, FromSqllog};
    use std::io::Write;
    use tempfile::NamedTempFile;

    struct TestRecord {
        timestamp: String,
        sql: String,
    }

    impl<'a> FromSqllog for TestRecord {
        fn from_sqllog(s: Sqllog<'a>) -> Self {
            TestRecord {
                timestamp: s.ts.to_string(),
                sql: s.body().to_string(),
            }
        }
    }
    ```

    测试 1（#[cfg(not(miri))]）: `test_from_sqllog_maps_record`
    - 写入一条有效记录到临时文件
    - 调用 `parser.iter().filter_map(|r| r.ok()).map(TestRecord::from_sqllog).collect::<Vec<_>>()`
    - 断言 `records.len() == 1`
    - 断言 `records[0].timestamp` 包含 "2025"
    - 断言 `records[0].sql` 包含相应的 SQL 文本

    测试 2（#[cfg(not(miri))]）: `test_exec_time_returns_value`
    - 写入一条含 EXECTIME 200ms 的有效记录
    - 解析后调用 `sqllog.exec_time()`
    - 断言结果为 `Ok(Some(200))` 或接近值（as u64 截断）

    测试 3（#[cfg(not(miri))]）: `test_exec_time_returns_none_when_missing`
    - 写入一条不含指标的记录
    - 解析后调用 `sqllog.exec_time()`
    - 断言结果为 `Ok(None)`

    测试 4（#[cfg(not(miri))]）: `test_row_count_returns_value`
    - 写入一条含 ROWCOUNT 42 行的有效记录
    - 解析后调用 `sqllog.row_count()`
    - 断言结果为 `Ok(Some(42))`

    测试 5（无 #[cfg(not(miri))] 门控）: `test_from_sqllog_trait_object_safety`
    - 编译时验证 FromSqllog 可以被作为 trait 引用
    - 定义一个接受 `&dyn FromSqllog` 的辅助函数（仅编译检查，不需要运行）

    测试记录格式（含 EXECTIME/ROWCOUNT）：
    ```
    2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:3 stmt:4 appname:a) SELECT 1 EXECTIME: 200(ms) ROWCOUNT: 42(rows) EXEC_ID: 1.
    ```

    不含指标的记录：
    ```
    2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:3 stmt:4 appname:a) SELECT 1
    ```

    每条记录以 `\n` 结尾。使用 `write!` 或 `file.write_all(content.as_bytes()).unwrap()` 写入文件。
  </action>
  <verify>
    <automated>cargo test --test parser_from_sqllog 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "pub trait FromSqllog" src/sqllog.rs` 输出为 1
    - `grep "FromSqllog" src/lib.rs` 至少 1 行（lib.rs 导出）
    - `grep -c "fn from_sqllog" src/sqllog.rs` 输出至少 1 行（trait 方法定义）
    - `grep -c "test_from_sqllog_maps_record" tests/parser_from_sqllog.rs` 输出为 1
    - `grep -c "test_exec_time_returns_value" tests/parser_from_sqllog.rs` 输出为 1
    - `grep -c "test_exec_time_returns_none_when_missing" tests/parser_from_sqllog.rs` 输出为 1
    - `grep -c "test_row_count_returns_value" tests/parser_from_sqllog.rs` 输出为 1
    - `grep -c "test_from_sqllog_trait_object_safety" tests/parser_from_sqllog.rs` 输出为 1
    - `cargo build` 退出码 0
    - `cargo test --test parser_from_sqllog` 5 个测试全部通过
    - `cargo test` 全 workspace 不破坏其他测试
    - `cargo clippy --all-targets -- -D warnings` 退出码 0
  </acceptance_criteria>
  <done>
    FromSqllog trait 定义完成，lib.rs 导出。exec_time/row_count 的 4 个测试覆盖有值、无值两种场景。FromSqllog 的 1 个测试覆盖映射流程。全部测试通过。
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| user FromSqllog impl -> Sqllog fields | 用户自定义 FromSqllog 实现可以访问 Sqllog 的全部 pub 字段和方法，属于预期设计 |
| exec_time/row_count -> parse_indicators | 方法内部调用 parse_indicators，该函数已存在且经过测试 |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-05 | Tampering | FromSqllog trait | accept | trait 定义只暴露 `from_sqllog` 一个方法，按值消费 Sqllog。用户实现自行决定映射逻辑，库不承担映射正确性责任。 |
| T-07-06 | Denial of Service | exec_time()/row_count() | accept | 内部调用 parse_indicators()，该函数已在 benchmarks 中验证性能开销 < 50ns。增加两个包装方法不会引入新瓶颈。 |
| T-07-SC | Tampering | 依赖注入 | mitigate | 不新增依赖。 |
</threat_model>

<verification>
- `cargo build` 编译通过
- `cargo test --test parser_from_sqllog` 全部测试通过
- `cargo test` 全 workspace 通过
- `cargo clippy --all-targets -- -D warnings` 无新增警告
- `cargo llvm-cov --workspace --all-features --fail-under-lines 90` 覆盖率 >= 90%
</verification>

<success_criteria>
- Sqllog::exec_time() 返回 `Result<Option<u64>, ParseError>`（EXECTIME 存在时 Ok(Some(v))，不存在时 Ok(None)）
- Sqllog::row_count() 返回 `Result<Option<u64>, ParseError>`（ROWCOUNT 存在时 Ok(Some(v))，不存在时 Ok(None)）
- 两个方法内部调用 parse_indicators()，不重复解析逻辑
- FromSqllog trait 定义：`pub trait FromSqllog { fn from_sqllog(s: Sqllog<'_>) -> Self; }`
- 用户类型可以 impl FromSqllog 并通过 `.map(MyType::from_sqllog)` 使用
- 5 个测试全部通过
</success_criteria>

<output>
After completion, create `.planning/phases/07-apiergonomics/07-03-SUMMARY.md` documenting:
- exec_time() / row_count() 签名、返回类型语义、内部实现
- FromSqllog trait 定义和使用方式
- 测试清单和覆盖场景
</output>
