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 #[cfg(feature = "odt")]
257 (Selector::OdtParagraph { .. }, R::Text | R::Metadata)
258 | (Selector::OdtHeading { .. }, R::Text | R::Metadata)
259 | (Selector::OdtTable { .. }, R::Text | R::Metadata)
260 | (Selector::OdtCell { .. }, R::Text | R::Metadata)
261 | (Selector::OdtList { .. }, R::Text | R::Metadata)
262 | (Selector::OdtFind { .. }, R::Text) => Ok(ObservePlan {
263 shape: PlanShape::DeepenThenObserve,
264 index_reads: 3,
265 required_nodes: 4,
266 will_materialize: kinds(&[
267 "OdtModel",
268 "OdtContent",
269 "PackageMemberDecoded",
270 "PackageMemberRaw",
271 ]),
272 will_not_materialize: kinds(&["other-parts", "whole-document"]),
273 }),
274 #[cfg(feature = "odt")]
275 (Selector::OdtPart(_), R::Metadata) => Ok(ObservePlan {
276 shape: PlanShape::DeepenThenObserve,
277 index_reads: 1,
278 required_nodes: 2,
279 will_materialize: kinds(&["OdtModel"]),
280 will_not_materialize: kinds(&["other-parts", "whole-document"]),
281 }),
282 #[cfg(feature = "odt")]
283 (Selector::OdtPart(_), R::ExactBytes | R::DecodedBytes) => Ok(ObservePlan {
284 shape: PlanShape::DeepenThenObserve,
285 index_reads: 2,
286 required_nodes: 3,
287 will_materialize: kinds(&["OdtModel", "PackageMemberRaw", "PackageMemberDecoded"]),
288 will_not_materialize: kinds(&["other-parts", "whole-document"]),
289 }),
290 _ => Err(Error::unsupported_feature(format!(
291 "unsupported observation: selector {} with representation {}",
292 req.selector.canonical(),
293 req.representation.name()
294 ))),
295 }
296}
297
298#[derive(Debug, Clone, Copy)]
299enum PageRepr {
300 Text,
301 Preview,
302 Structure,
303}
304
305fn kinds(names: &[&str]) -> Vec<String> {
306 names.iter().map(|s| (*s).to_string()).collect()
307}
308
309fn index_plan(kind: &str) -> ObservePlan {
310 ObservePlan {
311 shape: PlanShape::IndexLookup,
312 index_reads: 1,
313 required_nodes: 1,
314 will_materialize: kinds(&[kind]),
315 will_not_materialize: kinds(&["images", "xobjects", "PageContent", "whole-document"]),
316 }
317}
318
319fn common_plan(manifest: &FieldRoot, req: &ObserveRequest) -> Result<ObservePlan> {
322 use crate::field::capabilities;
323 let fmt = DocumentFormat::from_provenance(&manifest.provenance).ok_or_else(|| {
324 Error::unsupported_feature(
325 "field manifest does not record a document format; common observations are unavailable",
326 )
327 })?;
328 if !capabilities::common_supported(fmt, &req.selector, req.representation) {
329 return Err(Error::unsupported_feature(format!(
330 "unsupported common observation: format {} does not support selector {} with representation {}",
331 fmt.name(),
332 req.selector.canonical(),
333 req.representation.name()
334 )));
335 }
336 if matches!(req.selector, Selector::Metadata) {
339 return Ok(ObservePlan {
340 shape: PlanShape::CachedObservation,
341 index_reads: 0,
342 required_nodes: 0,
343 will_materialize: Vec::new(),
344 will_not_materialize: kinds(&["whole-document", "seed-nodes"]),
345 });
346 }
347 Ok(ObservePlan {
348 shape: PlanShape::DeepenThenObserve,
349 index_reads: 2,
350 required_nodes: 4,
351 will_materialize: kinds(common_materialize(fmt)),
352 will_not_materialize: kinds(&["other-spine-items", "other-stories", "whole-document"]),
353 })
354}
355
356fn common_materialize(fmt: DocumentFormat) -> &'static [&'static str] {
357 match fmt {
358 DocumentFormat::Pdf => &["PageContent", "TextRuns"],
359 DocumentFormat::Docx => &[
360 "DocxModel",
361 "DocxStory",
362 "PackageMemberDecoded",
363 "PackageMemberRaw",
364 ],
365 DocumentFormat::Epub => &[
366 "EpubModel",
367 "EpubContent",
368 "PackageMemberDecoded",
369 "PackageMemberRaw",
370 ],
371 DocumentFormat::Odt => &[
372 "OdtModel",
373 "OdtContent",
374 "PackageMemberDecoded",
375 "PackageMemberRaw",
376 ],
377 DocumentFormat::Opaque => &[],
378 }
379}
380
381fn index_entries(
382 manifest: &FieldRoot,
383 store: &FieldStore,
384 key: SelectorKey,
385) -> Result<Vec<IndexEntry>> {
386 if !manifest.has_index() {
387 return Ok(Vec::new());
388 }
389 let istore = FsIndexStore::open(store.root())?;
390 let root = NodeId::from_bytes(manifest.index_root);
391 lookup(&istore, &root, &key)
392}
393
394fn page_plan(
395 manifest: &FieldRoot,
396 store: &FieldStore,
397 page: u32,
398 repr: PageRepr,
399) -> Result<ObservePlan> {
400 let entry = index_entries(manifest, store, SelectorKey::new(SEL_PAGE, page))?
401 .into_iter()
402 .next()
403 .ok_or_else(|| {
404 Error::unsupported_feature(format!(
405 "no page matching selector number {page} in the observation index"
406 ))
407 })?;
408 let (_ops, text, preview) = derived_nodes(page, entry.node_id);
409 let target = match repr {
410 PageRepr::Text => text,
411 PageRepr::Preview | PageRepr::Structure => preview,
412 };
413 let present = store.seeds().contains_node(&target.content_id())?;
414 let shape = if present {
415 PlanShape::NodeMaterialize
416 } else {
417 PlanShape::DeepenThenObserve
418 };
419 let (materialize, required) = match repr {
420 PageRepr::Text => (kinds(&["PageContent", "ContentOperators", "TextRuns"]), 3),
421 PageRepr::Preview => (kinds(&["PageContent", "PagePreview"]), 2),
422 PageRepr::Structure => (
423 kinds(&["PageContent", "PagePreview", "ContentOperators"]),
424 3,
425 ),
426 };
427 Ok(ObservePlan {
428 shape,
429 index_reads: 1,
430 required_nodes: required,
431 will_materialize: materialize,
432 will_not_materialize: kinds(&["images", "xobjects", "whole-document"]),
433 })
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn shape_names_are_lower_snake_case() {
442 for shape in [
443 PlanShape::CachedObservation,
444 PlanShape::IndexLookup,
445 PlanShape::SourceSlice,
446 PlanShape::NodeMaterialize,
447 PlanShape::DeepenThenObserve,
448 PlanShape::FullMaterialize,
449 ] {
450 let name = shape.name();
451 assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
452 }
453 }
454}