Skip to main content

open_geocode/
pack.rs

1//! Pack writer and reader.
2//!
3//! A Pack is one container file (see [`crate::container`]) holding the record
4//! store, admin context table, text index and spatial index. Builds write a new
5//! generation next to the published one and switch the `CURRENT` pointer only
6//! after the new Pack passes validation, so running servers never see a
7//! partial Pack.
8//!
9//! ```text
10//! <pack dir>/CURRENT                         id of the published generation
11//! <pack dir>/generations/<id>/pack.ogp       the Pack
12//! <pack dir>/generations/<id>/audit/         build report and rejections
13//! ```
14
15use std::{
16    collections::BTreeMap,
17    fs,
18    io::{BufRead, BufReader},
19    path::{Path, PathBuf},
20    sync::Arc,
21    time::{SystemTime, UNIX_EPOCH},
22};
23
24use anyhow::{Context, Result, bail};
25use rayon::prelude::*;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28
29use crate::{
30    container::{Container, ContainerWriter},
31    context::{ContextReader, ContextTupleWriter},
32    extsort::Scratch,
33    memory::MemoryBudget,
34    record::{AddressRecord, InterpolationRecord, Layer, Record, RejectedRecord, StreetRecord},
35    records::{RecordsReader, RecordsWriter},
36    spatial_index::{RecordCells, SpatialIndexWriter},
37    text_index::TextIndexWriter,
38};
39
40pub use crate::{
41    context::{AdminContextTuple, RecordContext},
42    records::{ContextRecord, RecordPoint, RecordPointPrecision, RecordSource, RecordSummary},
43};
44
45pub type RecordId = u64;
46
47mod publication;
48pub use publication::resolve_pack_path;
49
50pub const PACK_FILE: &str = "pack.ogp";
51pub const AUDIT_DIR: &str = "audit";
52pub const REJECTIONS_FILE: &str = "rejections.jsonl";
53pub const BUILD_REPORT_FILE: &str = "build-report.json";
54pub const PACK_SCHEMA_VERSION: u32 = 6;
55
56const MANIFEST_SECTION: &str = "manifest";
57const MANIFEST_VERSION: u32 = 1;
58const SCRATCH_DIR: &str = ".scratch";
59/// Default memory for the spatial index sorter. The builder passes its own.
60const DEFAULT_SORT_BUDGET_BYTES: usize = 512 << 20;
61const DEFAULT_TEXT_INDEX_MEMORY_BYTES: usize = 1 << 30;
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct PackManifest {
65    pub schema_version: u32,
66    pub crate_version: String,
67    pub built_at_unix: u64,
68    pub record_count: u64,
69    pub layer_counts: BTreeMap<String, u64>,
70    pub string_count: u64,
71    pub context_tuple_count: u64,
72    pub text_document_count: u64,
73    pub spatial: PackSpatialManifest,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
77pub struct PackSpatialManifest {
78    pub point_count: u64,
79    pub segment_count: u64,
80    pub cell_count: u64,
81    pub context_cell_count: u64,
82}
83
84#[derive(Debug, Clone)]
85pub struct PackWriterOptions {
86    /// Directory for temporary build files. Defaults to a directory inside the
87    /// new generation.
88    pub scratch_dir: Option<PathBuf>,
89    /// Pool every buffer of the build reserves from.
90    pub memory: Arc<MemoryBudget>,
91    /// Share of the pool the spatial index sorter may hold before it spills.
92    pub sort_budget_bytes: usize,
93    /// Memory for Tantivy's indexing buffers, taken from the pool.
94    pub text_index_memory_bytes: usize,
95}
96
97impl Default for PackWriterOptions {
98    fn default() -> Self {
99        Self {
100            scratch_dir: None,
101            memory: MemoryBudget::unlimited(),
102            sort_budget_bytes: DEFAULT_SORT_BUDGET_BYTES,
103            text_index_memory_bytes: DEFAULT_TEXT_INDEX_MEMORY_BYTES,
104        }
105    }
106}
107
108/// Writes records in their final order. Record ids are assigned sequentially.
109pub struct PackWriter {
110    destination: PathBuf,
111    generation: GenerationGuard,
112    scratch: Arc<Scratch>,
113    records: RecordsWriter,
114    contexts: ContextTupleWriter,
115    text: TextIndexWriter,
116    spatial: SpatialIndexWriter,
117    layer_counts: BTreeMap<Layer, u64>,
118}
119
120/// A finished, unpublished Pack.
121pub struct SealedPack {
122    destination: PathBuf,
123    generation: GenerationGuard,
124    manifest: PackManifest,
125    bytes: u64,
126}
127
128/// Removes a generation that was never published: no reader can be using it.
129struct GenerationGuard {
130    path: PathBuf,
131    keep: bool,
132}
133
134impl Drop for GenerationGuard {
135    fn drop(&mut self) {
136        if !self.keep {
137            let _ = fs::remove_dir_all(&self.path);
138        }
139    }
140}
141
142/// Statistics a build reports after sealing the Pack.
143#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
144pub struct PackBuildStats {
145    pub bytes: u64,
146    pub sections: BTreeMap<String, u64>,
147    pub spatial_pair_runs: u64,
148    pub text_index_bytes: u64,
149}
150
151impl PackWriter {
152    pub fn create(destination: impl AsRef<Path>) -> Result<Self> {
153        Self::create_with(destination, PackWriterOptions::default())
154    }
155
156    pub fn create_with(destination: impl AsRef<Path>, options: PackWriterOptions) -> Result<Self> {
157        let destination = destination.as_ref().to_path_buf();
158        let generation = GenerationGuard {
159            path: publication::create_generation(&destination)?,
160            keep: false,
161        };
162        let scratch = Scratch::create(
163            options
164                .scratch_dir
165                .map(|dir| dir.join(generation.path.file_name().expect("generation name")))
166                .unwrap_or_else(|| generation.path.join(SCRATCH_DIR)),
167        )?;
168        Ok(Self {
169            records: RecordsWriter::create(&scratch, &options.memory)?,
170            contexts: ContextTupleWriter::default(),
171            text: TextIndexWriter::create(
172                &scratch,
173                &options.memory,
174                options.text_index_memory_bytes,
175            )?,
176            spatial: SpatialIndexWriter::new(&scratch, &options.memory, options.sort_budget_bytes),
177            scratch,
178            destination,
179            generation,
180            layer_counts: BTreeMap::new(),
181        })
182    }
183
184    /// Scratch directory shared with the builder's own sorters.
185    pub fn scratch(&self) -> &Arc<Scratch> {
186        &self.scratch
187    }
188
189    /// Directory of the generation being built, for build outputs that live
190    /// beside the Pack file.
191    pub fn generation(&self) -> &Path {
192        &self.generation.path
193    }
194
195    pub fn record_count(&self) -> u64 {
196        self.records.record_count()
197    }
198
199    pub fn write(&mut self, record: &Record, context: Option<RecordContext>) -> Result<RecordId> {
200        let first = self.records.record_count();
201        self.write_batch(std::slice::from_ref(&(record.clone(), context)))?;
202        Ok(first)
203    }
204
205    /// Write records in order. Text documents and spatial cells are computed in
206    /// parallel; the store itself is appended sequentially.
207    pub fn write_batch(&mut self, batch: &[(Record, Option<RecordContext>)]) -> Result<()> {
208        let first = self.records.record_count();
209        let fields = self.text.fields();
210        let prepared = batch
211            .par_iter()
212            .enumerate()
213            .map(|(offset, (record, _))| {
214                let record_id = first + offset as u64;
215                Ok((
216                    fields.document(record_id, record),
217                    RecordCells::for_record(record_id, record)?,
218                ))
219            })
220            .collect::<Result<Vec<_>>>()?;
221        for ((record, context), (document, cells)) in batch.iter().zip(prepared) {
222            let context = match context {
223                Some(context) => self.contexts.intern(*context)?,
224                None => None,
225            };
226            self.records.write(record, context)?;
227            self.text.add(document)?;
228            self.spatial.add(cells)?;
229            *self.layer_counts.entry(record.layer()).or_default() += 1;
230        }
231        Ok(())
232    }
233
234    /// Write the Pack file without publishing it.
235    pub fn seal(self) -> Result<(SealedPack, PackBuildStats)> {
236        let Self {
237            destination,
238            generation,
239            scratch,
240            records,
241            contexts,
242            text,
243            spatial,
244            layer_counts,
245        } = self;
246        let path = generation.path.join(PACK_FILE);
247        let mut pack = ContainerWriter::create(&path)?;
248        let record_count = records.record_count();
249        let string_count = records.string_count();
250        let context_tuple_count = contexts.tuple_count();
251
252        records.finish(&mut pack)?;
253        contexts.finish(&mut pack)?;
254        let text_commit = text.finish(&mut pack)?;
255        let spatial_commit = spatial.finish(&mut pack)?;
256
257        let manifest = PackManifest {
258            schema_version: PACK_SCHEMA_VERSION,
259            crate_version: env!("CARGO_PKG_VERSION").to_string(),
260            built_at_unix: SystemTime::now()
261                .duration_since(UNIX_EPOCH)
262                .map(|duration| duration.as_secs())
263                .unwrap_or_default(),
264            record_count,
265            layer_counts: layer_counts
266                .iter()
267                .map(|(layer, count)| (layer.as_str().to_string(), *count))
268                .collect(),
269            string_count,
270            context_tuple_count,
271            text_document_count: text_commit.document_count,
272            spatial: PackSpatialManifest {
273                point_count: spatial_commit.point_count,
274                segment_count: spatial_commit.segment_count,
275                cell_count: spatial_commit.cell_count,
276                context_cell_count: spatial_commit.context_cell_count,
277            },
278        };
279        pack.add(
280            MANIFEST_SECTION,
281            MANIFEST_VERSION,
282            &serde_json::to_vec_pretty(&manifest)?,
283        )?;
284        let bytes = pack.finish()?;
285        // Scratch files are no longer needed once the Pack file is complete.
286        drop(scratch);
287
288        let sections = Container::open(&path)?
289            .sections()
290            .iter()
291            .map(|(name, info)| (name.clone(), info.len))
292            .collect();
293        Ok((
294            SealedPack {
295                destination,
296                generation,
297                manifest,
298                bytes,
299            },
300            PackBuildStats {
301                bytes,
302                sections,
303                spatial_pair_runs: spatial_commit.fine_pairs.runs
304                    + spatial_commit.context_pairs.runs,
305                text_index_bytes: text_commit.bytes,
306            },
307        ))
308    }
309
310    /// Seal and publish.
311    pub fn finish(self) -> Result<PackManifest> {
312        self.seal()?.0.publish()
313    }
314}
315
316impl SealedPack {
317    pub fn generation(&self) -> &Path {
318        &self.generation.path
319    }
320
321    pub fn manifest(&self) -> &PackManifest {
322        &self.manifest
323    }
324
325    pub fn bytes(&self) -> u64 {
326        self.bytes
327    }
328
329    /// Validate the Pack with the serving readers, then point `CURRENT` at it.
330    pub fn publish(mut self) -> Result<PackManifest> {
331        publication::publish(
332            &self.destination,
333            &self.generation.path,
334            &self.manifest,
335            &mut self.generation.keep,
336        )?;
337        Ok(self.manifest)
338    }
339}
340
341pub struct PackReader {
342    path: PathBuf,
343    container: Container,
344    manifest: PackManifest,
345    records: RecordsReader,
346    context: ContextReader,
347}
348
349impl std::fmt::Debug for PackReader {
350    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        formatter
352            .debug_struct("PackReader")
353            .field("path", &self.path)
354            .field("manifest", &self.manifest)
355            .finish_non_exhaustive()
356    }
357}
358
359impl PackReader {
360    /// Open a Pack directory (its published generation) or a Pack file.
361    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
362        let path = resolve_pack_path(path)?;
363        let container = Container::open(&path)?;
364        let manifest: PackManifest =
365            serde_json::from_slice(&container.section(MANIFEST_SECTION, MANIFEST_VERSION)?)
366                .with_context(|| format!("failed to parse the manifest of {}", path.display()))?;
367        if manifest.schema_version != PACK_SCHEMA_VERSION {
368            bail!(
369                "Pack schema version {} is unsupported; rebuild the Pack for schema {}",
370                manifest.schema_version,
371                PACK_SCHEMA_VERSION
372            );
373        }
374        let records = RecordsReader::open(&container)?;
375        if records.record_count() != manifest.record_count {
376            bail!(
377                "record store has {} records but the manifest declares {}",
378                records.record_count(),
379                manifest.record_count
380            );
381        }
382        let context = ContextReader::open(&container)?;
383        Ok(Self {
384            path,
385            container,
386            manifest,
387            records,
388            context,
389        })
390    }
391
392    pub const fn manifest(&self) -> &PackManifest {
393        &self.manifest
394    }
395
396    /// The Pack file opened by this reader.
397    pub fn path(&self) -> &Path {
398        &self.path
399    }
400
401    pub fn container(&self) -> &Container {
402        &self.container
403    }
404
405    /// Check every section of the Pack file against its checksum.
406    pub fn verify(&self) -> Result<()> {
407        self.container
408            .verify()
409            .with_context(|| format!("{} failed verification", self.path.display()))
410    }
411
412    pub fn records(&self) -> &RecordsReader {
413        &self.records
414    }
415
416    /// Section sizes in bytes, by section name.
417    pub fn section_sizes(&self) -> BTreeMap<String, u64> {
418        self.container
419            .sections()
420            .iter()
421            .map(|(name, info)| (name.clone(), info.len))
422            .collect()
423    }
424
425    pub fn record_summary(&self, record_id: RecordId) -> Result<RecordSummary> {
426        self.records.summary(record_id)
427    }
428
429    pub fn record_json(&self, record_id: RecordId) -> Result<Value> {
430        self.records.record_json(record_id)
431    }
432
433    pub fn records_json_by_layer(&self, layer: &str, limit: usize) -> Result<Vec<Value>> {
434        let layer =
435            Layer::parse(layer).with_context(|| format!("unknown record layer: {layer}"))?;
436        let mut records = Vec::new();
437        for record_id in 0..self.records.record_count() {
438            if self.records.header(record_id)?.layer != layer {
439                continue;
440            }
441            records.push(self.record_json(record_id)?);
442            if limit > 0 && records.len() >= limit {
443                break;
444            }
445        }
446        Ok(records)
447    }
448
449    pub fn find_by_source_id(&self, source_id: &str) -> Result<Option<RecordId>> {
450        for record_id in 0..self.records.record_count() {
451            if self.record_summary(record_id)?.id == source_id {
452                return Ok(Some(record_id));
453            }
454        }
455        Ok(None)
456    }
457
458    pub fn address(&self, record_id: RecordId) -> Result<Option<AddressRecord>> {
459        Ok(match self.records.record(record_id)? {
460            Record::Address(record) => Some(record),
461            _ => None,
462        })
463    }
464
465    pub fn interpolation(&self, record_id: RecordId) -> Result<Option<InterpolationRecord>> {
466        if self.records.header(record_id)?.layer != Layer::Interpolation {
467            return Ok(None);
468        }
469        Ok(match self.records.record(record_id)? {
470            Record::Interpolation(record) => Some(record),
471            _ => None,
472        })
473    }
474
475    pub fn street(&self, record_id: RecordId) -> Result<Option<StreetRecord>> {
476        if self.records.header(record_id)?.layer != Layer::Street {
477            return Ok(None);
478        }
479        Ok(match self.records.record(record_id)? {
480            Record::Street(record) => Some(record),
481            _ => None,
482        })
483    }
484
485    pub fn context_record(&self, record_id: RecordId) -> Result<Option<ContextRecord>> {
486        self.records.context_record(record_id)
487    }
488
489    pub fn boundary_context(&self, record_id: RecordId) -> Result<Option<RecordContext>> {
490        self.records
491            .header(record_id)?
492            .context
493            .map(|reference| self.context.resolve(reference))
494            .transpose()
495    }
496
497    /// Rejected source objects recorded by the build that produced this Pack.
498    pub fn rejections(&self, limit: usize) -> Result<Vec<RejectedRecord>> {
499        let path = self
500            .path
501            .parent()
502            .map(|dir| dir.join(AUDIT_DIR).join(REJECTIONS_FILE))
503            .context("Pack file has no parent directory")?;
504        let file = fs::File::open(&path)
505            .with_context(|| format!("failed to open build audit {}", path.display()))?;
506        let mut rejections = Vec::new();
507        for line in BufReader::new(file).lines() {
508            if limit > 0 && rejections.len() >= limit {
509                break;
510            }
511            rejections.push(serde_json::from_str(&line?).context("invalid rejection record")?);
512        }
513        Ok(rejections)
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use std::io::Write;
520
521    use crate::{
522        record::{
523            AddressComponents, LocationPrecision, OsmObjectType, SourceProvenance, point_geometry,
524        },
525        search::{PackTextSearcher, TextSearchOptions},
526    };
527
528    use super::*;
529
530    fn temp_root(name: &str) -> PathBuf {
531        std::env::temp_dir().join(format!("open-geocode-{name}-{}", uuid::Uuid::new_v4()))
532    }
533
534    fn address_record(object_id: i64, number: &str, street: &str) -> Record {
535        Record::Address(AddressRecord {
536            address: AddressComponents {
537                number: number.to_string(),
538                street: Some(street.to_string()),
539                place: None,
540                unit: None,
541                locality: None,
542                region: None,
543                postcode: None,
544                country: None,
545            },
546            geometry: point_geometry(-79.0, 43.0),
547            location_precision: LocationPrecision::Point,
548            source: SourceProvenance::osm(OsmObjectType::Node, object_id),
549        })
550    }
551
552    fn write_pack(root: &Path, records: &[Record]) -> PackManifest {
553        let mut writer = PackWriter::create(root).expect("writer");
554        for record in records {
555            writer.write(record, None).expect("write");
556        }
557        writer.finish().expect("finish")
558    }
559
560    fn first_hit(root: &Path, query: &str) -> Option<String> {
561        PackTextSearcher::open(root)
562            .expect("searcher")
563            .search(TextSearchOptions {
564                query: query.into(),
565                limit: 1,
566                layer: None,
567            })
568            .expect("search")
569            .first()
570            .map(|hit| hit.record.id.clone())
571    }
572
573    #[test]
574    fn writes_one_file_and_reads_records_by_row_layer_and_source_id() {
575        let root = temp_root("pack");
576        let manifest = write_pack(
577            &root,
578            &[
579                address_record(1, "10", "King Street"),
580                address_record(2, "20", "Queen Street"),
581            ],
582        );
583        assert_eq!(manifest.record_count, 2);
584        assert_eq!(manifest.text_document_count, 2);
585        assert_eq!(manifest.spatial.point_count, 2);
586
587        let reader = PackReader::open(&root).expect("reader");
588        assert_eq!(
589            reader.path().file_name().and_then(|name| name.to_str()),
590            Some(PACK_FILE)
591        );
592        assert_eq!(reader.record_summary(1).expect("row 1").id, "osm:node:2");
593        assert_eq!(
594            reader.find_by_source_id("osm:node:1").expect("lookup"),
595            Some(0)
596        );
597        assert_eq!(
598            reader
599                .records_json_by_layer("address", 10)
600                .expect("layer")
601                .len(),
602            2
603        );
604        assert!(
605            reader
606                .records_json_by_layer("street", 10)
607                .expect("layer")
608                .is_empty()
609        );
610        assert!(reader.records_json_by_layer("nope", 10).is_err());
611        let sections = reader.section_sizes();
612        assert!(sections.contains_key("records/blocks"));
613        assert!(sections.keys().any(|name| name.starts_with("text/")));
614        assert!(sections.contains_key("spatial/fine/data"));
615        assert_eq!(first_hit(&root, "queen").as_deref(), Some("osm:node:2"));
616
617        // The Pack file alone is a complete Pack.
618        let copy = temp_root("copy").with_extension("ogp");
619        fs::copy(reader.path(), &copy).expect("copy");
620        assert_eq!(first_hit(&copy, "king").as_deref(), Some("osm:node:1"));
621        assert!(!reader.path().parent().unwrap().join(SCRATCH_DIR).exists());
622    }
623
624    #[test]
625    fn failed_osm_rebuild_preserves_published_pack() {
626        let root = temp_root("rebuild");
627        write_pack(&root, &[address_record(1, "10", "King Street")]);
628        let generations = fs::read_dir(root.join("generations")).expect("dir").count();
629
630        let result = crate::builder::build_osm_pack(crate::builder::BuildOsmOptions {
631            input: root.join("missing.osm.pbf"),
632            pack: root.clone(),
633            ..crate::builder::BuildOsmOptions::default()
634        });
635        assert!(result.is_err());
636        assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
637        assert_eq!(
638            fs::read_dir(root.join("generations")).expect("dir").count(),
639            generations,
640            "the failed generation is removed"
641        );
642    }
643
644    #[test]
645    fn publishes_replacement_without_changing_existing_readers() {
646        let root = temp_root("publish");
647        write_pack(&root, &[address_record(1, "10", "King Street")]);
648        let original = PackReader::open(&root).expect("original reader");
649        let searcher = PackTextSearcher::open(&root).expect("original searcher");
650
651        let mut replacement = PackWriter::create(&root).expect("replacement");
652        replacement
653            .write(&address_record(2, "20", "Queen Street"), None)
654            .expect("address");
655        assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
656        replacement.finish().expect("publish");
657
658        assert_eq!(first_hit(&root, "queen").as_deref(), Some("osm:node:2"));
659        assert_eq!(
660            original.record_summary(0).expect("original").id,
661            "osm:node:1"
662        );
663        let hits = searcher
664            .search(TextSearchOptions {
665                query: "king".into(),
666                limit: 5,
667                layer: None,
668            })
669            .expect("existing searcher after publish");
670        assert_eq!(hits[0].record.id, "osm:node:1");
671    }
672
673    #[test]
674    fn validation_failure_does_not_publish_replacement() {
675        let root = temp_root("validation");
676        write_pack(&root, &[address_record(1, "10", "King Street")]);
677        let published = fs::read(root.join("CURRENT")).expect("pointer");
678
679        let mut replacement = PackWriter::create(&root).expect("replacement");
680        replacement
681            .write(&address_record(2, "20", "Queen Street"), None)
682            .expect("address");
683        let (sealed, _) = replacement.seal().expect("seal");
684        let generation = sealed.generation().to_path_buf();
685        // Damage the table of contents of the unpublished Pack.
686        let pack = generation.join(PACK_FILE);
687        let mut bytes = fs::read(&pack).expect("read");
688        let len = bytes.len();
689        bytes[len - 1] ^= 0xff;
690        fs::File::create(&pack)
691            .and_then(|mut file| file.write_all(&bytes))
692            .expect("corrupt");
693        assert!(sealed.publish().is_err());
694        assert_eq!(fs::read(root.join("CURRENT")).expect("pointer"), published);
695        assert!(!generation.exists());
696        assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
697    }
698
699    #[test]
700    fn publishing_refuses_a_pack_corrupted_inside_a_section() {
701        let root = temp_root("checksum");
702        write_pack(&root, &[address_record(1, "10", "King Street")]);
703        let published = fs::read(root.join("CURRENT")).expect("pointer");
704
705        let mut replacement = PackWriter::create(&root).expect("replacement");
706        replacement
707            .write(&address_record(2, "20", "Queen Street"), None)
708            .expect("address");
709        let (sealed, _) = replacement.seal().expect("seal");
710        let pack = sealed.generation().join(PACK_FILE);
711        let offset = Container::open(&pack).expect("open").sections()["records/strings"].offset;
712        let mut bytes = fs::read(&pack).expect("read");
713        let last = bytes.len() - 1;
714        bytes[(offset as usize + 20).min(last)] ^= 0x55;
715        fs::write(&pack, &bytes).expect("corrupt");
716
717        let error = sealed.publish().unwrap_err();
718        assert!(format!("{error:#}").contains("checksum"), "{error:#}");
719        assert_eq!(fs::read(root.join("CURRENT")).expect("pointer"), published);
720    }
721
722    #[test]
723    fn a_failure_after_the_pointer_switch_keeps_the_published_generation() {
724        let root = temp_root("switch-failure");
725        write_pack(&root, &[address_record(1, "10", "King Street")]);
726        let mut replacement = PackWriter::create(&root).expect("replacement");
727        replacement
728            .write(&address_record(2, "20", "Queen Street"), None)
729            .expect("address");
730        let (sealed, _) = replacement.seal().expect("seal");
731
732        publication::FAIL_AFTER_SWITCH.set(true);
733        let result = sealed.publish();
734        publication::FAIL_AFTER_SWITCH.set(false);
735        assert!(result.is_err());
736        // CURRENT already names the new generation, so it must still exist.
737        assert_eq!(first_hit(&root, "queen").as_deref(), Some("osm:node:2"));
738    }
739
740    #[test]
741    fn text_index_memory_fits_the_budget_and_waits_for_records() {
742        let root = temp_root("text-memory");
743        let memory = MemoryBudget::new(64 << 20);
744        let mut writer = PackWriter::create_with(
745            &root,
746            PackWriterOptions {
747                scratch_dir: None,
748                memory: Arc::clone(&memory),
749                sort_budget_bytes: 16 << 20,
750                text_index_memory_bytes: 16 << 20,
751            },
752        )
753        .expect("writer");
754        assert_eq!(
755            memory.used(),
756            0,
757            "nothing is reserved before records arrive"
758        );
759        writer
760            .write(&address_record(1, "10", "King Street"), None)
761            .expect("address");
762        assert!(memory.used() > 0);
763        assert!(
764            memory.peak() <= memory.limit(),
765            "peak {} of {}",
766            memory.peak(),
767            memory.limit()
768        );
769        writer.finish().expect("finish");
770        assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
771    }
772
773    #[test]
774    fn rejects_paths_without_a_pack() {
775        let root = temp_root("empty");
776        fs::create_dir_all(&root).expect("dir");
777        assert!(PackReader::open(&root).is_err());
778    }
779}