1use crate::{AbsolutePath, ChangeSeq, CheckpointId, InodeId, NamespaceId, RevisionNo};
5use serde::{Deserialize, Serialize};
6use xxhash_rust::xxh64::xxh64;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
11pub struct GrepRequest {
12 pub pattern: String,
16 #[serde(default)]
19 pub case_insensitive: bool,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub path_prefix: Option<AbsolutePath>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub cursor: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub limit: Option<u32>,
33 #[serde(default)]
37 pub allow_stale: bool,
38 #[serde(default)]
41 pub allow_scan: bool,
42}
43
44impl GrepRequest {
45 pub fn fingerprint(&self) -> u64 {
49 let mut seed = xxh64(self.pattern.as_bytes(), 0);
50 seed = xxh64(
51 self.path_prefix
52 .as_ref()
53 .map(AbsolutePath::as_str)
54 .unwrap_or("")
55 .as_bytes(),
56 seed,
57 );
58 let flags = [
59 u8::from(self.case_insensitive),
60 u8::from(self.allow_stale),
61 u8::from(self.allow_scan),
62 ];
63 xxh64(&flags, seed)
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
70pub struct GrepMatch {
71 pub absolute_path: AbsolutePath,
73 pub inode_id: InodeId,
75 pub revision_no: RevisionNo,
77 pub line_number: u64,
79 pub byte_offset: u64,
81 pub line: String,
83 #[serde(default)]
85 pub line_truncated: bool,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
91pub struct GrepResponse {
92 pub namespace_id: NamespaceId,
94 pub head_seq: ChangeSeq,
98 pub built_through_seq: ChangeSeq,
100 pub tail_scanned: bool,
103 pub matches: Vec<GrepMatch>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub next_cursor: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
122#[serde(tag = "phase", rename_all = "snake_case")]
123pub enum GrepIndexLifecycle {
124 Disabled,
126 Backfilling {
129 target_seq: ChangeSeq,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
135 cursor_inode_id: Option<InodeId>,
136 checkpoint_id: CheckpointId,
138 },
139 Steady {
142 built_through_seq: ChangeSeq,
144 #[serde(default, skip_serializing_if = "is_zero")]
147 next_event_index: u32,
148 },
149}
150
151impl GrepIndexLifecycle {
152 pub fn is_built_through(&self, target_seq: ChangeSeq) -> bool {
158 match self {
159 Self::Disabled | Self::Backfilling { .. } => false,
160 Self::Steady {
161 built_through_seq,
162 next_event_index,
163 } => {
164 *built_through_seq > target_seq
165 || (*built_through_seq == target_seq && *next_event_index == 0)
166 }
167 }
168 }
169}
170
171fn is_zero(value: &u32) -> bool {
172 *value == 0
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
178pub struct EnableGrepIndexResponse {
179 pub namespace_id: NamespaceId,
181 pub already_enabled: bool,
183 pub state: GrepIndexLifecycle,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
191pub struct GrepIndexStatusResponse {
192 pub namespace_id: NamespaceId,
194 pub state: GrepIndexLifecycle,
196 pub next_run_ordinal: u64,
198 pub reorganize_pending: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
205pub struct DisableGrepIndexResponse {
206 pub namespace_id: NamespaceId,
208 pub was_enabled: bool,
210}
211
212#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
214#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
215pub struct GrepGcRequest {
216 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub max_objects: Option<u64>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub cursor: Option<String>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
230pub struct GrepGcResponse {
231 pub namespace_id: NamespaceId,
233 pub deleted_segments: u64,
235 pub deleted_other_objects: u64,
237 pub namespace_reaped: bool,
239 pub retained_candidates: u64,
241 pub namespace_degraded: bool,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub next_cursor: Option<String>,
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn grep_paths_keep_the_plain_string_wire_shape() {
254 let request = GrepRequest {
255 pattern: "needle".to_owned(),
256 case_insensitive: false,
257 path_prefix: Some(AbsolutePath::parse("/docs").expect("path prefix")),
258 cursor: None,
259 limit: None,
260 allow_stale: false,
261 allow_scan: false,
262 };
263 assert_eq!(
264 serde_json::to_value(request).expect("serialize grep request"),
265 serde_json::json!({
266 "pattern": "needle",
267 "case_insensitive": false,
268 "path_prefix": "/docs",
269 "allow_stale": false,
270 "allow_scan": false
271 })
272 );
273
274 let found = GrepMatch {
275 absolute_path: AbsolutePath::parse("/docs/a.txt").expect("match path"),
276 inode_id: InodeId(2),
277 revision_no: RevisionNo(3),
278 line_number: 4,
279 byte_offset: 5,
280 line: "needle".to_owned(),
281 line_truncated: false,
282 };
283 assert_eq!(
284 serde_json::to_value(found).expect("serialize grep match"),
285 serde_json::json!({
286 "absolute_path": "/docs/a.txt",
287 "inode_id": 2,
288 "revision_no": 3,
289 "line_number": 4,
290 "byte_offset": 5,
291 "line": "needle",
292 "line_truncated": false
293 })
294 );
295 }
296
297 #[test]
298 fn the_lifecycle_phases_never_share_a_sequence_field() {
299 let backfilling = GrepIndexLifecycle::Backfilling {
300 target_seq: ChangeSeq(9),
301 cursor_inode_id: Some(InodeId(4)),
302 checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
303 .expect("checkpoint id"),
304 };
305 assert_eq!(
306 serde_json::to_value(&backfilling).expect("serialize backfilling"),
307 serde_json::json!({
308 "phase": "backfilling",
309 "target_seq": 9,
310 "cursor_inode_id": 4,
311 "checkpoint_id": "chk_00000000000000000000000000000009"
312 }),
313 "a backfill reports its target and its walk, never a watermark"
314 );
315
316 assert_eq!(
317 serde_json::to_value(GrepIndexLifecycle::Steady {
318 built_through_seq: ChangeSeq(9),
319 next_event_index: 0,
320 })
321 .expect("serialize steady"),
322 serde_json::json!({"phase": "steady", "built_through_seq": 9}),
323 "a steady index reports its watermark and no target"
324 );
325
326 assert_eq!(
327 serde_json::to_value(GrepIndexLifecycle::Disabled).expect("serialize disabled"),
328 serde_json::json!({"phase": "disabled"})
329 );
330 }
331
332 #[test]
333 fn only_a_steady_index_has_built_through_a_sequence() {
334 let backfilling = GrepIndexLifecycle::Backfilling {
335 target_seq: ChangeSeq(9),
336 cursor_inode_id: None,
337 checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
338 .expect("checkpoint id"),
339 };
340 assert!(
341 !backfilling.is_built_through(ChangeSeq(0)),
342 "a backfill has indexed nothing until it turns steady"
343 );
344 assert!(!GrepIndexLifecycle::Disabled.is_built_through(ChangeSeq(0)));
345
346 let steady = |built_through_seq, next_event_index| GrepIndexLifecycle::Steady {
347 built_through_seq,
348 next_event_index,
349 };
350 assert!(steady(ChangeSeq(9), 0).is_built_through(ChangeSeq(9)));
351 assert!(steady(ChangeSeq(9), 0).is_built_through(ChangeSeq(8)));
352 assert!(!steady(ChangeSeq(9), 0).is_built_through(ChangeSeq(10)));
353 assert!(!steady(ChangeSeq(9), 3).is_built_through(ChangeSeq(9)));
356 assert!(steady(ChangeSeq(9), 3).is_built_through(ChangeSeq(8)));
357 }
358
359 #[test]
360 fn grep_path_prefix_validates_during_deserialization() {
361 let encoded = serde_json::json!({
362 "pattern": "needle",
363 "path_prefix": "relative/path"
364 });
365
366 assert!(serde_json::from_value::<GrepRequest>(encoded).is_err());
367 }
368}