1use crate::{AbsolutePath, ChangeSeq, CheckpointId, InodeId, NamespaceId, RevisionNo, RunNo};
4use serde::{Deserialize, Serialize};
5use xxhash_rust::xxh64::xxh64;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct GrepRequest {
10 pub pattern: String,
14 pub case_insensitive: bool,
16 pub path_prefix: Option<AbsolutePath>,
18 pub cursor: Option<String>,
21 pub allow_stale: bool,
24 pub allow_scan: bool,
27}
28
29impl GrepRequest {
30 pub fn fingerprint(&self) -> u64 {
34 let mut seed = xxh64(self.pattern.as_bytes(), 0);
35 seed = xxh64(
36 self.path_prefix
37 .as_ref()
38 .map_or("", AbsolutePath::as_str)
39 .as_bytes(),
40 seed,
41 );
42 let flags = [
43 u8::from(self.case_insensitive),
44 u8::from(self.allow_stale),
45 u8::from(self.allow_scan),
46 ];
47 xxh64(&flags, seed)
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
54pub struct GrepMatch {
55 pub path: AbsolutePath,
57 #[serde(with = "crate::public_inode_id")]
59 pub inode_id: InodeId,
60 pub revision_no: RevisionNo,
62 pub line_number: u64,
64 pub byte_offset: u64,
66 pub line: String,
68 pub line_truncated: bool,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
75pub struct GrepResponse {
76 pub namespace_id: NamespaceId,
78 pub head_seq: ChangeSeq,
80 pub built_through_seq: ChangeSeq,
82 pub tail_scanned: bool,
84 pub matches: Vec<GrepMatch>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 #[cfg_attr(feature = "openapi", schema(nullable = false))]
89 pub next_cursor: Option<String>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
97#[serde(tag = "status", rename_all = "snake_case")]
98pub enum GrepIndexLifecycle {
99 Disabled,
101 Backfilling {
103 target_seq: ChangeSeq,
105 #[serde(
107 default,
108 skip_serializing_if = "Option::is_none",
109 with = "crate::public_inode_id::option"
110 )]
111 #[cfg_attr(feature = "openapi", schema(nullable = false))]
112 cursor_inode_id: Option<InodeId>,
113 checkpoint_id: CheckpointId,
115 },
116 Active {
118 built_through_seq: ChangeSeq,
120 #[serde(default, skip_serializing_if = "is_zero")]
123 next_event_index: u32,
124 },
125}
126
127impl GrepIndexLifecycle {
128 pub fn is_built_through(&self, target_seq: ChangeSeq) -> bool {
134 match self {
135 Self::Disabled | Self::Backfilling { .. } => false,
136 Self::Active {
137 built_through_seq,
138 next_event_index,
139 } => {
140 *built_through_seq > target_seq
141 || (*built_through_seq == target_seq && *next_event_index == 0)
142 }
143 }
144 }
145}
146
147fn is_zero(value: &u32) -> bool {
148 *value == 0
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
154pub struct GrepIndex {
155 pub namespace_id: NamespaceId,
157 #[serde(flatten)]
159 pub lifecycle: GrepIndexLifecycle,
160 pub next_run_no: RunNo,
162 pub reorganize_pending: bool,
164}
165
166#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
168#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
169#[serde(deny_unknown_fields)]
170pub struct GrepGcRequest {}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
175pub struct GrepGcResponse {
176 pub namespace_id: NamespaceId,
178 pub deleted_segments: u64,
180 pub deleted_other_objects: u64,
182 pub namespace_reaped: bool,
184 pub retained_candidates: u64,
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn grep_paths_keep_the_plain_string_wire_shape() {
194 let found = GrepMatch {
195 path: AbsolutePath::parse("/docs/a.txt").expect("match path"),
196 inode_id: InodeId(2),
197 revision_no: RevisionNo(3),
198 line_number: 4,
199 byte_offset: 5,
200 line: "needle".to_owned(),
201 line_truncated: false,
202 };
203 assert_eq!(
204 serde_json::to_value(found).expect("serialize grep match"),
205 serde_json::json!({
206 "path": "/docs/a.txt",
207 "inode_id": "ino_2",
208 "revision_no": 3,
209 "line_number": 4,
210 "byte_offset": 5,
211 "line": "needle",
212 "line_truncated": false
213 })
214 );
215 }
216
217 #[test]
218 fn lifecycle_statuses_never_share_a_sequence_field() {
219 let backfilling = GrepIndexLifecycle::Backfilling {
220 target_seq: ChangeSeq(9),
221 cursor_inode_id: Some(InodeId(4)),
222 checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000009")
223 .expect("checkpoint id"),
224 };
225 assert_eq!(
226 serde_json::to_value(&backfilling).expect("serialize backfilling"),
227 serde_json::json!({
228 "status": "backfilling",
229 "target_seq": 9,
230 "cursor_inode_id": "ino_4",
231 "checkpoint_id": "pin_00000000000000000001-0000000000000009"
232 }),
233 "a backfill reports its target and its walk, never a watermark"
234 );
235
236 assert_eq!(
237 serde_json::to_value(GrepIndexLifecycle::Active {
238 built_through_seq: ChangeSeq(9),
239 next_event_index: 0,
240 })
241 .expect("serialize active"),
242 serde_json::json!({"status": "active", "built_through_seq": 9}),
243 "an active index reports its watermark and no target"
244 );
245
246 assert_eq!(
247 serde_json::to_value(GrepIndexLifecycle::Disabled).expect("serialize disabled"),
248 serde_json::json!({"status": "disabled"})
249 );
250 }
251
252 #[test]
253 fn only_an_active_index_has_built_through_a_sequence() {
254 let backfilling = GrepIndexLifecycle::Backfilling {
255 target_seq: ChangeSeq(9),
256 cursor_inode_id: None,
257 checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000009")
258 .expect("checkpoint id"),
259 };
260 assert!(
261 !backfilling.is_built_through(ChangeSeq(0)),
262 "a backfill has indexed nothing until it turns active"
263 );
264 assert!(!GrepIndexLifecycle::Disabled.is_built_through(ChangeSeq(0)));
265
266 let active = |built_through_seq, next_event_index| GrepIndexLifecycle::Active {
267 built_through_seq,
268 next_event_index,
269 };
270 assert!(active(ChangeSeq(9), 0).is_built_through(ChangeSeq(9)));
271 assert!(active(ChangeSeq(9), 0).is_built_through(ChangeSeq(8)));
272 assert!(!active(ChangeSeq(9), 0).is_built_through(ChangeSeq(10)));
273 assert!(!active(ChangeSeq(9), 3).is_built_through(ChangeSeq(9)));
276 assert!(active(ChangeSeq(9), 3).is_built_through(ChangeSeq(8)));
277 }
278
279 #[test]
280 fn grep_index_status_flattens_its_lifecycle() {
281 assert_eq!(
282 serde_json::to_value(GrepIndex {
283 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
284 lifecycle: GrepIndexLifecycle::Active {
285 built_through_seq: ChangeSeq(12),
286 next_event_index: 0,
287 },
288 next_run_no: RunNo(3),
289 reorganize_pending: false,
290 })
291 .expect("serialize active status"),
292 serde_json::json!({
293 "namespace_id": "demo",
294 "status": "active",
295 "built_through_seq": 12,
296 "next_run_no": 3,
297 "reorganize_pending": false
298 })
299 );
300
301 assert_eq!(
302 serde_json::to_value(GrepIndex {
303 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
304 lifecycle: GrepIndexLifecycle::Backfilling {
305 target_seq: ChangeSeq(12),
306 cursor_inode_id: Some(InodeId(4)),
307 checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000009")
308 .expect("checkpoint id"),
309 },
310 next_run_no: RunNo(1),
311 reorganize_pending: false,
312 })
313 .expect("serialize backfilling status"),
314 serde_json::json!({
315 "namespace_id": "demo",
316 "status": "backfilling",
317 "target_seq": 12,
318 "cursor_inode_id": "ino_4",
319 "checkpoint_id": "pin_00000000000000000001-0000000000000009",
320 "next_run_no": 1,
321 "reorganize_pending": false
322 })
323 );
324 }
325
326 #[test]
327 fn grep_gc_request_bodies_reject_unknown_fields() {
328 serde_json::from_value::<GrepGcRequest>(serde_json::json!({}))
329 .expect("an empty collection request decodes");
330 assert!(
331 serde_json::from_value::<GrepGcRequest>(serde_json::json!({"max_objects": 8})).is_err()
332 );
333 }
334}