1use crate::error::{Error, Result};
14use crate::field::FieldStore;
15use crate::field::index::{FsIndexStore, IndexEntry, SEL_PAGE, SelectorKey, lookup};
16use crate::field::manifest::FieldRoot;
17use crate::store::NodeId;
18
19use super::observe::{ObserveRequest, Representation, Selector, derived_nodes};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum PlanShape {
24 CachedObservation,
26 IndexLookup,
28 SourceSlice,
30 NodeMaterialize,
32 DeepenThenObserve,
34 FullMaterialize,
36}
37
38impl PlanShape {
39 pub const fn name(self) -> &'static str {
41 match self {
42 PlanShape::CachedObservation => "cached_observation",
43 PlanShape::IndexLookup => "index_lookup",
44 PlanShape::SourceSlice => "source_slice",
45 PlanShape::NodeMaterialize => "node_materialize",
46 PlanShape::DeepenThenObserve => "deepen_then_observe",
47 PlanShape::FullMaterialize => "full_materialize",
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ObservePlan {
57 pub shape: PlanShape,
59 pub index_reads: u64,
61 pub required_nodes: u64,
63 pub will_materialize: Vec<String>,
65 pub will_not_materialize: Vec<String>,
67}
68
69pub fn plan(manifest: &FieldRoot, store: &FieldStore, req: &ObserveRequest) -> Result<ObservePlan> {
71 use Representation as R;
72 match (&req.selector, req.representation) {
73 (Selector::Document, R::FullDocument | R::ExactBytes) => Ok(ObservePlan {
74 shape: PlanShape::FullMaterialize,
75 index_reads: 0,
76 required_nodes: 1,
77 will_materialize: kinds(&["DocumentExact"]),
78 will_not_materialize: Vec::new(),
79 }),
80 (Selector::Document, R::Metadata) => Ok(ObservePlan {
81 shape: PlanShape::CachedObservation,
82 index_reads: 0,
83 required_nodes: 0,
84 will_materialize: Vec::new(),
85 will_not_materialize: kinds(&["whole-document", "seed-nodes"]),
86 }),
87 (Selector::ByteRange { .. }, R::ExactBytes) => Ok(ObservePlan {
88 shape: PlanShape::SourceSlice,
89 index_reads: 0,
90 required_nodes: 0,
91 will_materialize: kinds(&["SourceSlice"]),
92 will_not_materialize: kinds(&["whole-document"]),
93 }),
94 (Selector::Object(_), R::ExactBytes | R::EncodedBytes) => Ok(index_plan("PdfObject")),
95 (Selector::Revision(_), R::ExactBytes) => Ok(index_plan("PdfRevision")),
96 (Selector::Stream(_), R::EncodedBytes) => Ok(index_plan("PdfStreamEncoded")),
97 (Selector::Stream(_), R::DecodedBytes) => Ok(ObservePlan {
98 shape: PlanShape::NodeMaterialize,
99 index_reads: 1,
100 required_nodes: 2,
101 will_materialize: kinds(&["PdfStreamDecoded", "PdfStreamEncoded"]),
102 will_not_materialize: kinds(&["images", "whole-document"]),
103 }),
104 (Selector::Stream(_), R::Operators) => Ok(ObservePlan {
105 shape: PlanShape::NodeMaterialize,
106 index_reads: 1,
107 required_nodes: 2,
108 will_materialize: kinds(&["PdfStreamDecoded", "ContentOperators"]),
109 will_not_materialize: kinds(&["images", "whole-document"]),
110 }),
111 (Selector::Page(n), R::Text) => page_plan(manifest, store, *n, PageRepr::Text),
112 (Selector::Page(n), R::Preview) => page_plan(manifest, store, *n, PageRepr::Preview),
113 (Selector::Page(n), R::Structure) => page_plan(manifest, store, *n, PageRepr::Structure),
114 (Selector::TextMatch(_), R::Text) => Ok(ObservePlan {
115 shape: PlanShape::DeepenThenObserve,
116 index_reads: 1,
117 required_nodes: 3,
118 will_materialize: kinds(&["PageContent", "ContentOperators", "TextRuns"]),
119 will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
120 }),
121 _ => Err(Error::unsupported_feature(format!(
122 "unsupported observation: selector {} with representation {}",
123 req.selector.canonical(),
124 req.representation.name()
125 ))),
126 }
127}
128
129#[derive(Debug, Clone, Copy)]
130enum PageRepr {
131 Text,
132 Preview,
133 Structure,
134}
135
136fn kinds(names: &[&str]) -> Vec<String> {
137 names.iter().map(|s| (*s).to_string()).collect()
138}
139
140fn index_plan(kind: &str) -> ObservePlan {
141 ObservePlan {
142 shape: PlanShape::IndexLookup,
143 index_reads: 1,
144 required_nodes: 1,
145 will_materialize: kinds(&[kind]),
146 will_not_materialize: kinds(&["images", "xobjects", "PageContent", "whole-document"]),
147 }
148}
149
150fn index_entries(
151 manifest: &FieldRoot,
152 store: &FieldStore,
153 key: SelectorKey,
154) -> Result<Vec<IndexEntry>> {
155 if !manifest.has_index() {
156 return Ok(Vec::new());
157 }
158 let istore = FsIndexStore::open(store.root())?;
159 let root = NodeId::from_bytes(manifest.index_root);
160 lookup(&istore, &root, &key)
161}
162
163fn page_plan(
164 manifest: &FieldRoot,
165 store: &FieldStore,
166 page: u32,
167 repr: PageRepr,
168) -> Result<ObservePlan> {
169 let entry = index_entries(manifest, store, SelectorKey::new(SEL_PAGE, page))?
170 .into_iter()
171 .next()
172 .ok_or_else(|| {
173 Error::unsupported_feature(format!(
174 "no page matching selector number {page} in the observation index"
175 ))
176 })?;
177 let (_ops, text, preview) = derived_nodes(page, entry.node_id);
178 let target = match repr {
179 PageRepr::Text => text,
180 PageRepr::Preview | PageRepr::Structure => preview,
181 };
182 let present = store.seeds().contains_node(&target.content_id())?;
183 let shape = if present {
184 PlanShape::NodeMaterialize
185 } else {
186 PlanShape::DeepenThenObserve
187 };
188 let (materialize, required) = match repr {
189 PageRepr::Text => (kinds(&["PageContent", "ContentOperators", "TextRuns"]), 3),
190 PageRepr::Preview => (kinds(&["PageContent", "PagePreview"]), 2),
191 PageRepr::Structure => (
192 kinds(&["PageContent", "PagePreview", "ContentOperators"]),
193 3,
194 ),
195 };
196 Ok(ObservePlan {
197 shape,
198 index_reads: 1,
199 required_nodes: required,
200 will_materialize: materialize,
201 will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
202 })
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn shape_names_are_lower_snake_case() {
211 for shape in [
212 PlanShape::CachedObservation,
213 PlanShape::IndexLookup,
214 PlanShape::SourceSlice,
215 PlanShape::NodeMaterialize,
216 PlanShape::DeepenThenObserve,
217 PlanShape::FullMaterialize,
218 ] {
219 let name = shape.name();
220 assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
221 }
222 }
223}