use crate::{AbsolutePath, ChangeSeq, CheckpointId, InodeId, NamespaceId, RevisionNo, RunNo};
use serde::{Deserialize, Serialize};
use xxhash_rust::xxh64::xxh64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrepRequest {
pub pattern: String,
pub case_insensitive: bool,
pub path_prefix: Option<AbsolutePath>,
pub cursor: Option<String>,
pub allow_stale: bool,
pub allow_scan: bool,
}
impl GrepRequest {
pub fn fingerprint(&self) -> u64 {
let mut seed = xxh64(self.pattern.as_bytes(), 0);
seed = xxh64(
self.path_prefix
.as_ref()
.map_or("", AbsolutePath::as_str)
.as_bytes(),
seed,
);
let flags = [
u8::from(self.case_insensitive),
u8::from(self.allow_stale),
u8::from(self.allow_scan),
];
xxh64(&flags, seed)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepMatch {
pub path: AbsolutePath,
#[serde(with = "crate::public_inode_id")]
pub inode_id: InodeId,
pub revision_no: RevisionNo,
pub line_number: u64,
pub byte_offset: u64,
pub line: String,
pub line_truncated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepResponse {
pub namespace_id: NamespaceId,
pub head_seq: ChangeSeq,
pub built_through_seq: ChangeSeq,
pub tail_scanned: bool,
pub matches: Vec<GrepMatch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum GrepIndexLifecycle {
Disabled,
Backfilling {
target_seq: ChangeSeq,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::public_inode_id::option"
)]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
cursor_inode_id: Option<InodeId>,
checkpoint_id: CheckpointId,
},
Active {
built_through_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "is_zero")]
next_event_index: u32,
},
}
impl GrepIndexLifecycle {
pub fn is_built_through(&self, target_seq: ChangeSeq) -> bool {
match self {
Self::Disabled | Self::Backfilling { .. } => false,
Self::Active {
built_through_seq,
next_event_index,
} => {
*built_through_seq > target_seq
|| (*built_through_seq == target_seq && *next_event_index == 0)
}
}
}
}
fn is_zero(value: &u32) -> bool {
*value == 0
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepIndex {
pub namespace_id: NamespaceId,
#[serde(flatten)]
pub lifecycle: GrepIndexLifecycle,
pub next_run_no: RunNo,
pub reorganize_pending: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct GrepGcRequest {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepGcResponse {
pub namespace_id: NamespaceId,
pub deleted_segments: u64,
pub deleted_other_objects: u64,
pub namespace_reaped: bool,
pub retained_candidates: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grep_paths_keep_the_plain_string_wire_shape() {
let found = GrepMatch {
path: AbsolutePath::parse("/docs/a.txt").expect("match path"),
inode_id: InodeId(2),
revision_no: RevisionNo(3),
line_number: 4,
byte_offset: 5,
line: "needle".to_owned(),
line_truncated: false,
};
assert_eq!(
serde_json::to_value(found).expect("serialize grep match"),
serde_json::json!({
"path": "/docs/a.txt",
"inode_id": "ino_2",
"revision_no": 3,
"line_number": 4,
"byte_offset": 5,
"line": "needle",
"line_truncated": false
})
);
}
#[test]
fn lifecycle_statuses_never_share_a_sequence_field() {
let backfilling = GrepIndexLifecycle::Backfilling {
target_seq: ChangeSeq(9),
cursor_inode_id: Some(InodeId(4)),
checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000009")
.expect("checkpoint id"),
};
assert_eq!(
serde_json::to_value(&backfilling).expect("serialize backfilling"),
serde_json::json!({
"status": "backfilling",
"target_seq": 9,
"cursor_inode_id": "ino_4",
"checkpoint_id": "pin_00000000000000000001-0000000000000009"
}),
"a backfill reports its target and its walk, never a watermark"
);
assert_eq!(
serde_json::to_value(GrepIndexLifecycle::Active {
built_through_seq: ChangeSeq(9),
next_event_index: 0,
})
.expect("serialize active"),
serde_json::json!({"status": "active", "built_through_seq": 9}),
"an active index reports its watermark and no target"
);
assert_eq!(
serde_json::to_value(GrepIndexLifecycle::Disabled).expect("serialize disabled"),
serde_json::json!({"status": "disabled"})
);
}
#[test]
fn only_an_active_index_has_built_through_a_sequence() {
let backfilling = GrepIndexLifecycle::Backfilling {
target_seq: ChangeSeq(9),
cursor_inode_id: None,
checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000009")
.expect("checkpoint id"),
};
assert!(
!backfilling.is_built_through(ChangeSeq(0)),
"a backfill has indexed nothing until it turns active"
);
assert!(!GrepIndexLifecycle::Disabled.is_built_through(ChangeSeq(0)));
let active = |built_through_seq, next_event_index| GrepIndexLifecycle::Active {
built_through_seq,
next_event_index,
};
assert!(active(ChangeSeq(9), 0).is_built_through(ChangeSeq(9)));
assert!(active(ChangeSeq(9), 0).is_built_through(ChangeSeq(8)));
assert!(!active(ChangeSeq(9), 0).is_built_through(ChangeSeq(10)));
assert!(!active(ChangeSeq(9), 3).is_built_through(ChangeSeq(9)));
assert!(active(ChangeSeq(9), 3).is_built_through(ChangeSeq(8)));
}
#[test]
fn grep_index_status_flattens_its_lifecycle() {
assert_eq!(
serde_json::to_value(GrepIndex {
namespace_id: NamespaceId::parse("demo").expect("namespace id"),
lifecycle: GrepIndexLifecycle::Active {
built_through_seq: ChangeSeq(12),
next_event_index: 0,
},
next_run_no: RunNo(3),
reorganize_pending: false,
})
.expect("serialize active status"),
serde_json::json!({
"namespace_id": "demo",
"status": "active",
"built_through_seq": 12,
"next_run_no": 3,
"reorganize_pending": false
})
);
assert_eq!(
serde_json::to_value(GrepIndex {
namespace_id: NamespaceId::parse("demo").expect("namespace id"),
lifecycle: GrepIndexLifecycle::Backfilling {
target_seq: ChangeSeq(12),
cursor_inode_id: Some(InodeId(4)),
checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000009")
.expect("checkpoint id"),
},
next_run_no: RunNo(1),
reorganize_pending: false,
})
.expect("serialize backfilling status"),
serde_json::json!({
"namespace_id": "demo",
"status": "backfilling",
"target_seq": 12,
"cursor_inode_id": "ino_4",
"checkpoint_id": "pin_00000000000000000001-0000000000000009",
"next_run_no": 1,
"reorganize_pending": false
})
);
}
#[test]
fn grep_gc_request_bodies_reject_unknown_fields() {
serde_json::from_value::<GrepGcRequest>(serde_json::json!({}))
.expect("an empty collection request decodes");
assert!(
serde_json::from_value::<GrepGcRequest>(serde_json::json!({"max_objects": 8})).is_err()
);
}
}