
# 查询

`RecordQuery` 是与 `stream()` 配合使用的流式筛选器。它涵盖追加式访问模式：从游标处继续读取、读取时间窗口、按字段相等条件筛选，以及在达到限制时停止。对于更丰富的谓词、排序和基于索引的查找，请使用[查询构建器](https://storh.inline0.dev/zh/docs/query-builder)；对于普通 `foreach` 在数据流上可执行的所有操作，`RecordQuery` 是开销更低的工具。

`RecordQuery` 是不可变的。每个修饰方法都会返回一个克隆的查询：

```php
$query = Storh\RecordQuery::all()
    ->after($lastSeenId)
    ->time_range_ms($fromMs, $untilMs)
    ->where_equal('kind', 'page')
    ->limit(100);

foreach ($store->stream($query) as $record) {
    // Records are yielded lazily.
}
```

`after()` 和 `time_range_ms()` 使用 UUID 排序，因此记录数据中无需包含时间戳字段。在 `SegmentedLog` 上，它们还会跳过无关工作：ID 范围超出窗口的分段不会被打开；每段的稀疏索引会直接定位到接近首条匹配记录的位置，无需从头扫描。

`where_equal()` 会将顶层数据字段与 `===` 进行比较，并接受标量值或 `null`。`null` 筛选器会匹配字段存在且值为 null 的记录；不含该字段的记录不会匹配。

## 游标分页

ID 按创建时间排序，因此一页的最后一个 ID 就是下一页的游标：

```php
$page = iterator_to_array($store->stream(
    Storh\RecordQuery::all()->after($lastSeenId)->limit(100)
), false);

$lastSeenId = [] === $page ? $lastSeenId : end($page)->id();
```

## 跳过损坏记录

默认情况下，扫描会在遇到第一条无法读取的记录时抛出错误。如果扫描应跳过该记录并报告错误，请使用
`continue_on_error()`：

```php
$query = Storh\RecordQuery::all()->continue_on_error(
    static function (string $location, Throwable $error): void {
        error_log($location . ': ' . $error->getMessage());
    },
);
```

对于 `DocStore`，位置是记录 ID；对于 `SegmentedLog`，位置为 `segment-file:byte-offset`。`repair()` 才是实际清理损坏记录的工具；参见[CLI](https://storh.inline0.dev/zh/docs/cli)。
