1use crate::{AbsolutePath, ChangeSeq, CheckpointId, InodeId, NamespaceId, RevisionNo, RunNo};
5use serde::{Deserialize, Serialize};
6use xxhash_rust::xxh64::xxh64;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct GrepRequest {
11 pub pattern: String,
16 pub case_insensitive: bool,
19 pub path_prefix: Option<AbsolutePath>,
22 pub cursor: Option<String>,
27 pub allow_stale: bool,
32 pub allow_scan: bool,
35}
36
37impl GrepRequest {
38 pub fn fingerprint(&self) -> u64 {
42 let mut seed = xxh64(self.pattern.as_bytes(), 0);
43 seed = xxh64(
44 self.path_prefix
45 .as_ref()
46 .map_or("", AbsolutePath::as_str)
47 .as_bytes(),
48 seed,
49 );
50 let flags = [
51 u8::from(self.case_insensitive),
52 u8::from(self.allow_stale),
53 u8::from(self.allow_scan),
54 ];
55 xxh64(&flags, seed)
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
62pub struct GrepMatch {
63 pub path: AbsolutePath,
65 #[serde(with = "crate::public_inode_id")]
67 #[cfg_attr(
68 feature = "openapi",
69 schema(schema_with = crate::public_inode_id::schema)
70 )]
71 pub inode_id: InodeId,
72 pub revision_no: RevisionNo,
74 pub line_number: u64,
76 pub byte_offset: u64,
78 pub line: String,
80 pub line_truncated: bool,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
87pub struct GrepResponse {
88 pub namespace_id: NamespaceId,
90 pub head_seq: ChangeSeq,
94 pub built_through_seq: ChangeSeq,
96 pub tail_scanned: bool,
99 pub matches: Vec<GrepMatch>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub next_cursor: Option<String>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
117#[serde(tag = "status", rename_all = "snake_case")]
118pub enum GrepIndexLifecycle {
119 Disabled,
121 Backfilling {
124 target_seq: ChangeSeq,
127 #[serde(
130 default,
131 skip_serializing_if = "Option::is_none",
132 with = "crate::public_inode_id::option"
133 )]
134 #[cfg_attr(
135 feature = "openapi",
136 schema(schema_with = crate::public_inode_id::schema)
137 )]
138 cursor_inode_id: Option<InodeId>,
139 checkpoint_id: CheckpointId,
141 },
142 Active {
145 built_through_seq: ChangeSeq,
147 #[serde(default, skip_serializing_if = "is_zero")]
150 next_event_index: u32,
151 },
152}
153
154impl GrepIndexLifecycle {
155 pub fn is_built_through(&self, target_seq: ChangeSeq) -> bool {
161 match self {
162 Self::Disabled | Self::Backfilling { .. } => false,
163 Self::Active {
164 built_through_seq,
165 next_event_index,
166 } => {
167 *built_through_seq > target_seq
168 || (*built_through_seq == target_seq && *next_event_index == 0)
169 }
170 }
171 }
172}
173
174fn is_zero(value: &u32) -> bool {
175 *value == 0
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
182pub struct GrepIndex {
183 pub namespace_id: NamespaceId,
185 #[serde(flatten)]
187 pub lifecycle: GrepIndexLifecycle,
188 pub next_run_no: RunNo,
190 pub reorganize_pending: bool,
192}
193
194#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
196#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
197#[serde(deny_unknown_fields)]
198pub struct GrepGcRequest {
199 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub max_objects: Option<u64>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub cursor: Option<String>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
213pub struct GrepGcResponse {
214 pub namespace_id: NamespaceId,
216 pub deleted_segments: u64,
218 pub deleted_other_objects: u64,
220 pub namespace_reaped: bool,
222 pub retained_candidates: u64,
224 pub namespace_degraded: bool,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub next_cursor: Option<String>,
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn grep_paths_keep_the_plain_string_wire_shape() {
237 let found = GrepMatch {
238 path: AbsolutePath::parse("/docs/a.txt").expect("match path"),
239 inode_id: InodeId(2),
240 revision_no: RevisionNo(3),
241 line_number: 4,
242 byte_offset: 5,
243 line: "needle".to_owned(),
244 line_truncated: false,
245 };
246 assert_eq!(
247 serde_json::to_value(found).expect("serialize grep match"),
248 serde_json::json!({
249 "path": "/docs/a.txt",
250 "inode_id": "ino_2",
251 "revision_no": 3,
252 "line_number": 4,
253 "byte_offset": 5,
254 "line": "needle",
255 "line_truncated": false
256 })
257 );
258 }
259
260 #[test]
261 fn lifecycle_statuses_never_share_a_sequence_field() {
262 let backfilling = GrepIndexLifecycle::Backfilling {
263 target_seq: ChangeSeq(9),
264 cursor_inode_id: Some(InodeId(4)),
265 checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
266 .expect("checkpoint id"),
267 };
268 assert_eq!(
269 serde_json::to_value(&backfilling).expect("serialize backfilling"),
270 serde_json::json!({
271 "status": "backfilling",
272 "target_seq": 9,
273 "cursor_inode_id": "ino_4",
274 "checkpoint_id": "chk_00000000000000000000000000000009"
275 }),
276 "a backfill reports its target and its walk, never a watermark"
277 );
278
279 assert_eq!(
280 serde_json::to_value(GrepIndexLifecycle::Active {
281 built_through_seq: ChangeSeq(9),
282 next_event_index: 0,
283 })
284 .expect("serialize active"),
285 serde_json::json!({"status": "active", "built_through_seq": 9}),
286 "an active index reports its watermark and no target"
287 );
288
289 assert_eq!(
290 serde_json::to_value(GrepIndexLifecycle::Disabled).expect("serialize disabled"),
291 serde_json::json!({"status": "disabled"})
292 );
293 }
294
295 #[test]
296 fn only_an_active_index_has_built_through_a_sequence() {
297 let backfilling = GrepIndexLifecycle::Backfilling {
298 target_seq: ChangeSeq(9),
299 cursor_inode_id: None,
300 checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
301 .expect("checkpoint id"),
302 };
303 assert!(
304 !backfilling.is_built_through(ChangeSeq(0)),
305 "a backfill has indexed nothing until it turns active"
306 );
307 assert!(!GrepIndexLifecycle::Disabled.is_built_through(ChangeSeq(0)));
308
309 let active = |built_through_seq, next_event_index| GrepIndexLifecycle::Active {
310 built_through_seq,
311 next_event_index,
312 };
313 assert!(active(ChangeSeq(9), 0).is_built_through(ChangeSeq(9)));
314 assert!(active(ChangeSeq(9), 0).is_built_through(ChangeSeq(8)));
315 assert!(!active(ChangeSeq(9), 0).is_built_through(ChangeSeq(10)));
316 assert!(!active(ChangeSeq(9), 3).is_built_through(ChangeSeq(9)));
319 assert!(active(ChangeSeq(9), 3).is_built_through(ChangeSeq(8)));
320 }
321
322 #[test]
323 fn grep_index_status_flattens_its_lifecycle() {
324 assert_eq!(
325 serde_json::to_value(GrepIndex {
326 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
327 lifecycle: GrepIndexLifecycle::Active {
328 built_through_seq: ChangeSeq(12),
329 next_event_index: 0,
330 },
331 next_run_no: RunNo(3),
332 reorganize_pending: false,
333 })
334 .expect("serialize active status"),
335 serde_json::json!({
336 "namespace_id": "demo",
337 "status": "active",
338 "built_through_seq": 12,
339 "next_run_no": 3,
340 "reorganize_pending": false
341 })
342 );
343
344 assert_eq!(
345 serde_json::to_value(GrepIndex {
346 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
347 lifecycle: GrepIndexLifecycle::Backfilling {
348 target_seq: ChangeSeq(12),
349 cursor_inode_id: Some(InodeId(4)),
350 checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
351 .expect("checkpoint id"),
352 },
353 next_run_no: RunNo(1),
354 reorganize_pending: false,
355 })
356 .expect("serialize backfilling status"),
357 serde_json::json!({
358 "namespace_id": "demo",
359 "status": "backfilling",
360 "target_seq": 12,
361 "cursor_inode_id": "ino_4",
362 "checkpoint_id": "chk_00000000000000000000000000000009",
363 "next_run_no": 1,
364 "reorganize_pending": false
365 })
366 );
367 }
368
369 #[test]
370 fn grep_gc_request_bodies_reject_unknown_fields() {
371 serde_json::from_value::<GrepGcRequest>(serde_json::json!({"max_objects": 8}))
372 .expect("the same collection body without a typo decodes");
373 assert!(
374 serde_json::from_value::<GrepGcRequest>(serde_json::json!({"maxObjects": 8})).is_err()
375 );
376 }
377}