1use crate::error::{Error, Result};
14use crate::field::FieldStore;
15use crate::field::document_format::DocumentFormat;
16use crate::field::index::{FsIndexStore, IndexEntry, SEL_PAGE, SelectorKey, lookup};
17use crate::field::manifest::FieldRoot;
18use crate::store::NodeId;
19
20use super::observe::{ObserveRequest, Representation, Selector, derived_nodes};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum PlanShape {
25 CachedObservation,
27 IndexLookup,
29 SourceSlice,
31 NodeMaterialize,
33 DeepenThenObserve,
35 FullMaterialize,
37}
38
39impl PlanShape {
40 pub const fn name(self) -> &'static str {
42 match self {
43 PlanShape::CachedObservation => "cached_observation",
44 PlanShape::IndexLookup => "index_lookup",
45 PlanShape::SourceSlice => "source_slice",
46 PlanShape::NodeMaterialize => "node_materialize",
47 PlanShape::DeepenThenObserve => "deepen_then_observe",
48 PlanShape::FullMaterialize => "full_materialize",
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ObservePlan {
58 pub shape: PlanShape,
60 pub index_reads: u64,
62 pub required_nodes: u64,
64 pub will_materialize: Vec<String>,
66 pub will_not_materialize: Vec<String>,
68}
69
70pub fn plan(manifest: &FieldRoot, store: &FieldStore, req: &ObserveRequest) -> Result<ObservePlan> {
72 if req.selector.is_common() {
76 return common_plan(manifest, req);
77 }
78 use Representation as R;
79 match (&req.selector, req.representation) {
80 (Selector::Document, R::FullDocument | R::ExactBytes) => Ok(ObservePlan {
81 shape: PlanShape::FullMaterialize,
82 index_reads: 0,
83 required_nodes: 1,
84 will_materialize: kinds(&["DocumentExact"]),
85 will_not_materialize: Vec::new(),
86 }),
87 (Selector::Document, R::Metadata) => Ok(ObservePlan {
88 shape: PlanShape::CachedObservation,
89 index_reads: 0,
90 required_nodes: 0,
91 will_materialize: Vec::new(),
92 will_not_materialize: kinds(&["whole-document", "seed-nodes"]),
93 }),
94 (Selector::ByteRange { .. }, R::ExactBytes) => Ok(ObservePlan {
95 shape: PlanShape::SourceSlice,
96 index_reads: 0,
97 required_nodes: 0,
98 will_materialize: kinds(&["SourceSlice"]),
99 will_not_materialize: kinds(&["whole-document"]),
100 }),
101 (Selector::Object(_), R::ExactBytes | R::EncodedBytes) => Ok(index_plan("PdfObject")),
102 (Selector::Revision(_), R::ExactBytes) => Ok(index_plan("PdfRevision")),
103 (Selector::Member(_), R::EncodedBytes) => Ok(ObservePlan {
104 shape: PlanShape::IndexLookup,
105 index_reads: 1,
106 required_nodes: 1,
107 will_materialize: kinds(&["PackageMemberRaw"]),
108 will_not_materialize: kinds(&["other-members", "whole-document"]),
109 }),
110 (Selector::Member(_), R::DecodedBytes) => Ok(ObservePlan {
111 shape: PlanShape::NodeMaterialize,
112 index_reads: 1,
113 required_nodes: 2,
114 will_materialize: kinds(&["PackageMemberDecoded", "PackageMemberRaw"]),
115 will_not_materialize: kinds(&["other-members", "whole-document"]),
116 }),
117 (Selector::PackagePart(_), R::Metadata) => Ok(ObservePlan {
118 shape: PlanShape::DeepenThenObserve,
119 index_reads: 1,
120 required_nodes: 2,
121 will_materialize: kinds(&["PackageOpcModel"]),
122 will_not_materialize: kinds(&["other-parts", "whole-document"]),
123 }),
124 (Selector::PackagePart(_), R::ExactBytes) => Ok(ObservePlan {
125 shape: PlanShape::DeepenThenObserve,
126 index_reads: 2,
127 required_nodes: 3,
128 will_materialize: kinds(&["PackageOpcModel", "PackageMemberRaw"]),
129 will_not_materialize: kinds(&["other-parts", "whole-document"]),
130 }),
131 (Selector::PackagePart(_), R::DecodedBytes) => Ok(ObservePlan {
132 shape: PlanShape::DeepenThenObserve,
133 index_reads: 2,
134 required_nodes: 4,
135 will_materialize: kinds(&[
136 "PackageOpcModel",
137 "PackageMemberDecoded",
138 "PackageMemberRaw",
139 ]),
140 will_not_materialize: kinds(&["other-parts", "whole-document"]),
141 }),
142 (Selector::Relationship(_), R::Metadata) => Ok(ObservePlan {
143 shape: PlanShape::DeepenThenObserve,
144 index_reads: 1,
145 required_nodes: 2,
146 will_materialize: kinds(&["PackageOpcModel"]),
147 will_not_materialize: kinds(&["other-relationships", "whole-document"]),
148 }),
149 (Selector::Relationship(_), R::ExactBytes) => Ok(ObservePlan {
150 shape: PlanShape::DeepenThenObserve,
151 index_reads: 2,
152 required_nodes: 3,
153 will_materialize: kinds(&["PackageOpcModel", "PackageMemberRaw"]),
154 will_not_materialize: kinds(&["external-targets", "whole-document"]),
155 }),
156 (Selector::Relationship(_), R::DecodedBytes) => Ok(ObservePlan {
157 shape: PlanShape::DeepenThenObserve,
158 index_reads: 2,
159 required_nodes: 4,
160 will_materialize: kinds(&[
161 "PackageOpcModel",
162 "PackageMemberDecoded",
163 "PackageMemberRaw",
164 ]),
165 will_not_materialize: kinds(&["external-targets", "whole-document"]),
166 }),
167 (Selector::Stream(_), R::EncodedBytes) => Ok(index_plan("PdfStreamEncoded")),
168 (Selector::Stream(_), R::DecodedBytes) => Ok(ObservePlan {
169 shape: PlanShape::NodeMaterialize,
170 index_reads: 1,
171 required_nodes: 2,
172 will_materialize: kinds(&["PdfStreamDecoded", "PdfStreamEncoded"]),
173 will_not_materialize: kinds(&["images", "whole-document"]),
174 }),
175 (Selector::Stream(_), R::Operators) => Ok(ObservePlan {
176 shape: PlanShape::NodeMaterialize,
177 index_reads: 1,
178 required_nodes: 2,
179 will_materialize: kinds(&["PdfStreamDecoded", "ContentOperators"]),
180 will_not_materialize: kinds(&["images", "whole-document"]),
181 }),
182 (Selector::Page(n), R::Text) => page_plan(manifest, store, *n, PageRepr::Text),
183 (Selector::Page(n), R::Preview) => page_plan(manifest, store, *n, PageRepr::Preview),
184 (Selector::Page(n), R::Structure) => page_plan(manifest, store, *n, PageRepr::Structure),
185 (Selector::TextMatch(_), R::Text) => Ok(ObservePlan {
186 shape: PlanShape::DeepenThenObserve,
187 index_reads: 1,
188 required_nodes: 3,
189 will_materialize: kinds(&["PageContent", "ContentOperators", "TextRuns"]),
190 will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
191 }),
192 #[cfg(feature = "docx")]
193 (Selector::DocxStory { .. }, R::Text | R::Structure | R::Metadata)
194 | (Selector::DocxParagraph { .. }, R::Text | R::Metadata)
195 | (Selector::DocxTable { .. }, R::Text | R::Metadata)
196 | (Selector::DocxCell { .. }, R::Text | R::Metadata)
197 | (Selector::DocxFind { .. }, R::Text) => Ok(ObservePlan {
198 shape: PlanShape::DeepenThenObserve,
199 index_reads: 3,
200 required_nodes: 4,
201 will_materialize: kinds(&[
202 "DocxModel",
203 "DocxStory",
204 "PackageMemberDecoded",
205 "PackageMemberRaw",
206 ]),
207 will_not_materialize: kinds(&["other-stories", "whole-document"]),
208 }),
209 #[cfg(feature = "epub")]
210 (Selector::EpubPackage, R::Metadata | R::Structure)
211 | (Selector::EpubNav, R::Metadata | R::Structure)
212 | (Selector::EpubNavNode { .. }, R::Metadata) => Ok(ObservePlan {
213 shape: PlanShape::DeepenThenObserve,
214 index_reads: 1,
215 required_nodes: 3,
216 will_materialize: kinds(&["EpubModel", "PackageMemberDecoded", "PackageMemberRaw"]),
217 will_not_materialize: kinds(&["other-resources", "whole-document"]),
218 }),
219 #[cfg(feature = "epub")]
220 (Selector::EpubManifestItem { .. }, R::Metadata)
221 | (Selector::EpubSpineItem { .. }, R::Metadata)
222 | (Selector::EpubResource(_), R::Metadata) => Ok(ObservePlan {
223 shape: PlanShape::DeepenThenObserve,
224 index_reads: 1,
225 required_nodes: 2,
226 will_materialize: kinds(&["EpubModel"]),
227 will_not_materialize: kinds(&["external-targets", "whole-document"]),
228 }),
229 #[cfg(feature = "epub")]
230 (Selector::EpubSpineItem { .. }, R::Text | R::Structure | R::Preview)
231 | (Selector::EpubBlock { .. }, R::Text | R::Metadata | R::Structure)
232 | (Selector::EpubCell { .. }, R::Text | R::Metadata)
233 | (Selector::EpubLink { .. }, R::Metadata)
234 | (Selector::EpubFind { .. }, R::Text) => Ok(ObservePlan {
235 shape: PlanShape::DeepenThenObserve,
236 index_reads: 2,
237 required_nodes: 4,
238 will_materialize: kinds(&[
239 "EpubModel",
240 "EpubContent",
241 "PackageMemberDecoded",
242 "PackageMemberRaw",
243 ]),
244 will_not_materialize: kinds(&["other-spine-items", "whole-document"]),
245 }),
246 #[cfg(feature = "epub")]
247 (Selector::EpubManifestItem { .. }, R::ExactBytes | R::DecodedBytes)
248 | (Selector::EpubSpineItem { .. }, R::ExactBytes | R::DecodedBytes)
249 | (Selector::EpubResource(_), R::ExactBytes | R::DecodedBytes) => Ok(ObservePlan {
250 shape: PlanShape::DeepenThenObserve,
251 index_reads: 2,
252 required_nodes: 3,
253 will_materialize: kinds(&["EpubModel", "PackageMemberRaw", "PackageMemberDecoded"]),
254 will_not_materialize: kinds(&["external-targets", "whole-document"]),
255 }),
256 _ => Err(Error::unsupported_feature(format!(
257 "unsupported observation: selector {} with representation {}",
258 req.selector.canonical(),
259 req.representation.name()
260 ))),
261 }
262}
263
264#[derive(Debug, Clone, Copy)]
265enum PageRepr {
266 Text,
267 Preview,
268 Structure,
269}
270
271fn kinds(names: &[&str]) -> Vec<String> {
272 names.iter().map(|s| (*s).to_string()).collect()
273}
274
275fn index_plan(kind: &str) -> ObservePlan {
276 ObservePlan {
277 shape: PlanShape::IndexLookup,
278 index_reads: 1,
279 required_nodes: 1,
280 will_materialize: kinds(&[kind]),
281 will_not_materialize: kinds(&["images", "xobjects", "PageContent", "whole-document"]),
282 }
283}
284
285fn common_plan(manifest: &FieldRoot, req: &ObserveRequest) -> Result<ObservePlan> {
288 use crate::field::capabilities;
289 let fmt = DocumentFormat::from_provenance(&manifest.provenance).ok_or_else(|| {
290 Error::unsupported_feature(
291 "field manifest does not record a document format; common observations are unavailable",
292 )
293 })?;
294 if !capabilities::common_supported(fmt, &req.selector, req.representation) {
295 return Err(Error::unsupported_feature(format!(
296 "unsupported common observation: format {} does not support selector {} with representation {}",
297 fmt.name(),
298 req.selector.canonical(),
299 req.representation.name()
300 )));
301 }
302 if matches!(req.selector, Selector::Metadata) {
305 return Ok(ObservePlan {
306 shape: PlanShape::CachedObservation,
307 index_reads: 0,
308 required_nodes: 0,
309 will_materialize: Vec::new(),
310 will_not_materialize: kinds(&["whole-document", "seed-nodes"]),
311 });
312 }
313 Ok(ObservePlan {
314 shape: PlanShape::DeepenThenObserve,
315 index_reads: 2,
316 required_nodes: 4,
317 will_materialize: kinds(common_materialize(fmt)),
318 will_not_materialize: kinds(&["other-spine-items", "other-stories", "whole-document"]),
319 })
320}
321
322fn common_materialize(fmt: DocumentFormat) -> &'static [&'static str] {
323 match fmt {
324 DocumentFormat::Pdf => &["PageContent", "TextRuns"],
325 DocumentFormat::Docx => &[
326 "DocxModel",
327 "DocxStory",
328 "PackageMemberDecoded",
329 "PackageMemberRaw",
330 ],
331 DocumentFormat::Epub => &[
332 "EpubModel",
333 "EpubContent",
334 "PackageMemberDecoded",
335 "PackageMemberRaw",
336 ],
337 DocumentFormat::Opaque => &[],
338 }
339}
340
341fn index_entries(
342 manifest: &FieldRoot,
343 store: &FieldStore,
344 key: SelectorKey,
345) -> Result<Vec<IndexEntry>> {
346 if !manifest.has_index() {
347 return Ok(Vec::new());
348 }
349 let istore = FsIndexStore::open(store.root())?;
350 let root = NodeId::from_bytes(manifest.index_root);
351 lookup(&istore, &root, &key)
352}
353
354fn page_plan(
355 manifest: &FieldRoot,
356 store: &FieldStore,
357 page: u32,
358 repr: PageRepr,
359) -> Result<ObservePlan> {
360 let entry = index_entries(manifest, store, SelectorKey::new(SEL_PAGE, page))?
361 .into_iter()
362 .next()
363 .ok_or_else(|| {
364 Error::unsupported_feature(format!(
365 "no page matching selector number {page} in the observation index"
366 ))
367 })?;
368 let (_ops, text, preview) = derived_nodes(page, entry.node_id);
369 let target = match repr {
370 PageRepr::Text => text,
371 PageRepr::Preview | PageRepr::Structure => preview,
372 };
373 let present = store.seeds().contains_node(&target.content_id())?;
374 let shape = if present {
375 PlanShape::NodeMaterialize
376 } else {
377 PlanShape::DeepenThenObserve
378 };
379 let (materialize, required) = match repr {
380 PageRepr::Text => (kinds(&["PageContent", "ContentOperators", "TextRuns"]), 3),
381 PageRepr::Preview => (kinds(&["PageContent", "PagePreview"]), 2),
382 PageRepr::Structure => (
383 kinds(&["PageContent", "PagePreview", "ContentOperators"]),
384 3,
385 ),
386 };
387 Ok(ObservePlan {
388 shape,
389 index_reads: 1,
390 required_nodes: required,
391 will_materialize: materialize,
392 will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
393 })
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn shape_names_are_lower_snake_case() {
402 for shape in [
403 PlanShape::CachedObservation,
404 PlanShape::IndexLookup,
405 PlanShape::SourceSlice,
406 PlanShape::NodeMaterialize,
407 PlanShape::DeepenThenObserve,
408 PlanShape::FullMaterialize,
409 ] {
410 let name = shape.name();
411 assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
412 }
413 }
414}