Skip to main content

mq_db/
store.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    sync::{Mutex, RwLock},
5};
6
7/// In-memory state for a user-defined table.
8///
9/// `first_row_page`/`last_row_page` track where this table's rows live in
10/// the backing storage file (0 = not persisted yet), so a SQL `INSERT`
11/// can append just the new rows to the chain instead of rewriting `rows`
12/// in full on every call. See [`Storage::write_table_rows`].
13pub(crate) struct CustomTableState {
14    pub columns: Vec<String>,
15    pub rows: Vec<Vec<String>>,
16    pub first_row_page: u32,
17    pub last_row_page: u32,
18}
19
20use mq_markdown::Markdown;
21
22use crate::{
23    block::DocumentId,
24    document::Document,
25    error::MqdbError,
26    index,
27    indexes::DocumentIndex,
28    query::Query,
29    storage::{
30        Storage,
31        catalog::{CatalogEntry, CustomTableEntry},
32        codec::{decode_zone_map, encode_zone_map},
33    },
34};
35
36/// Persists any table whose rows have never been written to `storage` (i.e.
37/// `first_row_page == 0`), then builds catalog entries for every table.
38///
39/// Tables that already have a row-page chain are left untouched here — their
40/// pages were already written by an earlier flush or incremental `INSERT`
41/// append (see [`DocumentStore::try_append_table_rows_to_storage`]).
42fn persist_unsaved_table_rows(
43    storage: &mut Storage,
44    custom_tables: &RwLock<HashMap<String, CustomTableState>>,
45) -> Result<Vec<CustomTableEntry>, MqdbError> {
46    let mut guard = custom_tables.write().unwrap();
47    for state in guard.values_mut() {
48        if state.first_row_page == 0 && !state.rows.is_empty() {
49            let (first, last) = storage.write_table_rows(&state.rows)?;
50            state.first_row_page = first;
51            state.last_row_page = last;
52        }
53    }
54    Ok(guard
55        .iter()
56        .map(|(name, state)| CustomTableEntry {
57            name: name.clone(),
58            columns: state.columns.clone(),
59            first_row_page: state.first_row_page,
60            last_row_page: state.last_row_page,
61            num_rows: state.rows.len() as u32,
62        })
63        .collect())
64}
65
66/// The top-level embedded document store.
67///
68/// Holds a collection of parsed Markdown documents and provides access to
69/// the query interface. Documents are stored in memory with their flattened
70/// block lists and interval indexes.
71///
72/// ## Load modes
73///
74/// - [`DocumentStore::new`] / [`DocumentStore::add_str`] — in-memory, blocks immediately available
75/// - [`DocumentStore::load`] — reads all blocks from a `.mq-db` file into memory
76/// - [`DocumentStore::open`] — reads catalog only; blocks loaded on demand via
77///   [`load_all_blocks`](DocumentStore::load_all_blocks)
78///
79/// Secondary indexes ([`DocumentIndex`]) are built once via
80/// [`load_all_indexes`](DocumentStore::load_all_indexes) and cached, so
81/// subsequent [`crate::SqlEngine`] construction is O(1).
82///
83/// # Example
84///
85/// ```rust
86/// use mq_db::DocumentStore;
87///
88/// let mut store = DocumentStore::new();
89/// store.add_str("# Hello\n\nWorld").unwrap();
90///
91/// let results = store.query().heading_depth(1).blocks();
92/// assert_eq!(results.len(), 1);
93/// assert_eq!(results[0].content, "Hello");
94/// ```
95pub struct DocumentStore {
96    documents: Vec<Document>,
97    next_doc_id: DocumentId,
98    /// When `false`, source line/column spans are discarded after parsing.
99    store_spans: bool,
100    /// Open storage file kept for lazy block / index loading. `None` when the
101    /// store was built entirely in memory or fully loaded via `load()`.
102    /// Wrapped in `Mutex` so DDL operations (which hold only `&DocumentStore`)
103    /// can flush the updated catalog to disk.
104    pub(crate) storage: Mutex<Option<Storage>>,
105    /// Per-document secondary index cache (same order as `documents`).
106    /// `None` means the index has not been built/loaded for that document yet.
107    pub(crate) doc_indexes: Vec<Option<DocumentIndex>>,
108    /// User-registered virtual tables: name → (columns, rows).
109    /// Uses `RwLock` for interior mutability so `SqlEngine` can execute DDL
110    /// (`CREATE TABLE`, `INSERT INTO`, `DROP TABLE`) with only `&DocumentStore`.
111    pub(crate) custom_tables: RwLock<HashMap<String, CustomTableState>>,
112}
113
114impl Default for DocumentStore {
115    fn default() -> Self {
116        Self {
117            documents: Vec::new(),
118            next_doc_id: 0,
119            store_spans: true,
120            storage: Mutex::new(None),
121            doc_indexes: Vec::new(),
122            custom_tables: RwLock::new(HashMap::new()),
123        }
124    }
125}
126
127impl DocumentStore {
128    /// Creates an empty document store.
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// When set to `false`, source line/column spans are stripped from every
134    /// block added after this call. Reduces memory by ~21 bytes per block.
135    pub fn set_store_spans(&mut self, val: bool) {
136        self.store_spans = val;
137    }
138
139    /// Register a custom virtual table that can be queried via SQL.
140    ///
141    /// The table is queryable with `SELECT … FROM <name>`. All column values
142    /// are treated as strings; cast them in SQL as needed.
143    ///
144    /// Calling this a second time with the same name replaces the previous table.
145    pub fn register_table(
146        &mut self,
147        name: impl Into<String>,
148        columns: Vec<String>,
149        rows: Vec<Vec<String>>,
150    ) {
151        self.custom_tables.write().unwrap().insert(
152            name.into(),
153            CustomTableState {
154                columns,
155                rows,
156                first_row_page: 0,
157                last_row_page: 0,
158            },
159        );
160    }
161
162    /// Remove a previously registered custom table. Returns `true` if it existed.
163    pub fn unregister_table(&mut self, name: &str) -> bool {
164        self.custom_tables.write().unwrap().remove(name).is_some()
165    }
166
167    /// Parses and adds a Markdown file from disk.
168    ///
169    /// Returns the assigned `DocumentId` on success.
170    pub fn add_file(&mut self, path: impl AsRef<Path>) -> Result<DocumentId, MqdbError> {
171        let path = path.as_ref();
172        let content = std::fs::read_to_string(path)?;
173        self.add_str_with_path(&content, Some(path.to_path_buf()))
174    }
175
176    /// Parses and adds Markdown content from a string.
177    ///
178    /// Returns the assigned `DocumentId` on success.
179    pub fn add_str(&mut self, content: &str) -> Result<DocumentId, MqdbError> {
180        self.add_str_with_path(content, None)
181    }
182
183    /// Parses and adds already-read Markdown content, attributing it to
184    /// `path`. For callers that read files concurrently and want to skip
185    /// [`add_file`](Self::add_file)'s own read.
186    pub fn add_str_with_path(
187        &mut self,
188        content: &str,
189        path: Option<std::path::PathBuf>,
190    ) -> Result<DocumentId, MqdbError> {
191        let md =
192            Markdown::from_markdown_str(content).map_err(|e| MqdbError::Parse(e.to_string()))?;
193
194        let doc_id = self.next_doc_id;
195        self.next_doc_id += 1;
196
197        let mut blocks = index::build_blocks(doc_id, &md.nodes);
198        if !self.store_spans {
199            for block in &mut blocks {
200                block.span = None;
201            }
202        }
203        let doc = Document::new(doc_id, path, blocks);
204        self.documents.push(doc);
205        self.doc_indexes.push(None);
206
207        Ok(doc_id)
208    }
209
210    /// Append a Markdown string to the existing `.mq-db` file (in-place).
211    ///
212    /// Works only when the store was opened via [`DocumentStore::open`] (i.e.
213    /// `self.storage` is `Some`).  New block pages and an index page chain are
214    /// appended to the file and the catalog is rewritten to include the new
215    /// entry.
216    ///
217    /// When called on an in-memory store (no backing file) this behaves
218    /// identically to [`add_str`](DocumentStore::add_str).
219    pub fn append_str(&mut self, content: &str) -> Result<DocumentId, MqdbError> {
220        self.do_append(content, None)
221    }
222
223    /// Append a Markdown file to the existing `.mq-db` file (in-place).
224    ///
225    /// See [`append_str`](DocumentStore::append_str) for full semantics.
226    pub fn append_file(&mut self, path: impl AsRef<Path>) -> Result<DocumentId, MqdbError> {
227        let path = path.as_ref();
228        let content = std::fs::read_to_string(path)?;
229        self.do_append(&content, Some(path.to_path_buf()))
230    }
231
232    fn do_append(
233        &mut self,
234        content: &str,
235        md_path: Option<PathBuf>,
236    ) -> Result<DocumentId, MqdbError> {
237        let md =
238            Markdown::from_markdown_str(content).map_err(|e| MqdbError::Parse(e.to_string()))?;
239        let doc_id = self.next_doc_id;
240        self.next_doc_id += 1;
241
242        let mut blocks = index::build_blocks(doc_id, &md.nodes);
243        if !self.store_spans {
244            for block in &mut blocks {
245                block.span = None;
246            }
247        }
248        let mut doc = Document::new(doc_id, md_path, blocks);
249
250        let idx_opt = {
251            let mut storage_guard = self.storage.lock().unwrap();
252            if let Some(storage) = storage_guard.as_mut() {
253                // Reconstruct catalog entries from already-loaded document metadata.
254                let mut entries = self.catalog_entries();
255
256                let first_block_page = storage.write_document(&doc)?;
257                doc.first_block_page = first_block_page;
258
259                let idx = DocumentIndex::build(&doc.blocks);
260                let index_start_page = storage.write_index(&idx.to_bytes())?;
261                doc.index_start_page = index_start_page;
262
263                entries.push(CatalogEntry {
264                    document_id: doc.id,
265                    path: doc.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
266                    first_block_page,
267                    num_blocks: doc.block_count,
268                    zone_map_bytes: encode_zone_map(&doc.zone_maps),
269                    index_start_page,
270                });
271
272                let custom = persist_unsaved_table_rows(storage, &self.custom_tables)?;
273                storage.flush_catalog(&entries, &custom)?;
274                Some(idx)
275            } else {
276                None
277            }
278        };
279        self.doc_indexes.push(idx_opt);
280
281        self.documents.push(doc);
282        Ok(doc_id)
283    }
284
285    /// Returns a slice of all documents in the store.
286    pub fn documents(&self) -> &[Document] {
287        &self.documents
288    }
289
290    /// Looks up a document by its `DocumentId`.
291    pub fn get_document(&self, id: DocumentId) -> Option<&Document> {
292        self.documents.iter().find(|d| d.id == id)
293    }
294
295    /// Returns the number of documents in the store.
296    pub fn len(&self) -> usize {
297        self.documents.len()
298    }
299
300    /// Returns `true` if the store contains no documents.
301    pub fn is_empty(&self) -> bool {
302        self.documents.is_empty()
303    }
304
305    /// Creates a new query builder backed by this store.
306    pub fn query(&self) -> Query<'_> {
307        Query::new(self)
308    }
309
310    // ─────────────────────────────────────────────────────────────────────────
311    // Lazy loading
312    // ─────────────────────────────────────────────────────────────────────────
313
314    /// Load blocks for every document that has not yet been loaded.
315    ///
316    /// No-op when the store was built in memory or fully loaded via `load()`.
317    pub fn load_all_blocks(&mut self) -> Result<(), MqdbError> {
318        let mut guard = self.storage.lock().unwrap();
319        let storage = match guard.as_mut() {
320            Some(s) => s,
321            None => return Ok(()),
322        };
323        for doc in &mut self.documents {
324            if doc.blocks.is_empty() && doc.block_count > 0 {
325                doc.blocks = storage.read_blocks(doc.first_block_page, doc.block_count)?;
326            }
327        }
328        Ok(())
329    }
330
331    /// Build or load persisted secondary indexes for every document and cache them.
332    ///
333    /// Must be called after [`load_all_blocks`](DocumentStore::load_all_blocks).
334    /// Subsequent [`crate::SqlEngine`] construction reuses the cache and pays no
335    /// per-block index rebuild cost.
336    pub fn load_all_indexes(&mut self) -> Result<(), MqdbError> {
337        for i in 0..self.documents.len() {
338            if self.doc_indexes[i].is_some() {
339                continue;
340            }
341
342            let idx = self.build_or_load_index_at(i)?;
343            self.doc_indexes[i] = Some(idx);
344        }
345        Ok(())
346    }
347
348    fn build_or_load_index_at(&mut self, i: usize) -> Result<DocumentIndex, MqdbError> {
349        let index_start_page = self.documents[i].index_start_page;
350
351        if index_start_page > 0 {
352            let mut guard = self.storage.lock().unwrap();
353            if let Some(storage) = guard.as_mut() {
354                let bytes = storage.read_index_bytes(index_start_page)?;
355                return DocumentIndex::from_bytes(&bytes);
356            }
357        }
358
359        Ok(DocumentIndex::build(&self.documents[i].blocks))
360    }
361
362    /// Returns the cached `DocumentIndex` for the document at position `i`.
363    pub(crate) fn get_doc_index(&self, i: usize) -> Option<&DocumentIndex> {
364        self.doc_indexes.get(i).and_then(|o| o.as_ref())
365    }
366
367    /// Builds catalog entries for every in-memory document.
368    fn catalog_entries(&self) -> Vec<CatalogEntry> {
369        self.documents
370            .iter()
371            .map(|d| CatalogEntry {
372                document_id: d.id,
373                path: d.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
374                first_block_page: d.first_block_page,
375                num_blocks: d.block_count,
376                zone_map_bytes: encode_zone_map(&d.zone_maps),
377                index_start_page: d.index_start_page,
378            })
379            .collect()
380    }
381
382    /// Flush the catalog (including custom tables) to the backing storage file,
383    /// if one is open. Called automatically after DDL operations such as
384    /// `CREATE TABLE` and `DROP TABLE`. No-op for in-memory stores.
385    ///
386    /// Any table whose rows have never been persisted is written out in full
387    /// here (a one-time cost). Tables already backed by a row-page chain keep
388    /// their existing pages untouched — see
389    /// [`try_append_table_rows_to_storage`](DocumentStore::try_append_table_rows_to_storage)
390    /// for the incremental `INSERT` path.
391    pub(crate) fn try_flush_catalog_to_storage(&self) {
392        let mut guard = self.storage.lock().unwrap();
393        if let Some(storage) = guard.as_mut() {
394            let entries = self.catalog_entries();
395            if let Ok(custom) = persist_unsaved_table_rows(storage, &self.custom_tables) {
396                let _ = storage.flush_catalog(&entries, &custom);
397            }
398        }
399    }
400
401    /// Append `new_rows` to `table_name`'s on-disk row chain and flush a
402    /// lightweight catalog update — no full row rewrite. No-op for in-memory
403    /// stores or unknown tables.
404    ///
405    /// This is what makes `INSERT INTO <table>` incremental: the cost is
406    /// proportional to the rows being inserted, not to the table's total size.
407    pub(crate) fn try_append_table_rows_to_storage(
408        &self,
409        table_name: &str,
410        new_rows: &[Vec<String>],
411    ) {
412        let mut guard = self.storage.lock().unwrap();
413        let storage = match guard.as_mut() {
414            Some(s) => s,
415            None => return,
416        };
417
418        {
419            let mut ct_guard = self.custom_tables.write().unwrap();
420            if let Some(state) = ct_guard.get_mut(table_name) {
421                let persisted = if state.first_row_page == 0 {
422                    // Nothing persisted yet for this table — write everything
423                    // currently in memory (covers rows seeded via
424                    // `register_table` plus the ones just inserted).
425                    storage.write_table_rows(&state.rows)
426                } else {
427                    storage
428                        .append_table_rows(state.last_row_page, new_rows)
429                        .map(|last| (state.first_row_page, last))
430                };
431                if let Ok((first, last)) = persisted {
432                    state.first_row_page = first;
433                    state.last_row_page = last;
434                }
435            }
436        }
437
438        let entries = self.catalog_entries();
439        let ct_guard = self.custom_tables.read().unwrap();
440        let custom: Vec<CustomTableEntry> = ct_guard
441            .iter()
442            .map(|(name, state)| CustomTableEntry {
443                name: name.clone(),
444                columns: state.columns.clone(),
445                first_row_page: state.first_row_page,
446                last_row_page: state.last_row_page,
447                num_rows: state.rows.len() as u32,
448            })
449            .collect();
450        drop(ct_guard);
451        let _ = storage.flush_catalog(&entries, &custom);
452    }
453
454    // ─────────────────────────────────────────────────────────────────────────
455    // Persistence
456    // ─────────────────────────────────────────────────────────────────────────
457
458    /// Persist all in-memory documents to a `.mq-db` file, including secondary
459    /// indexes. Writes atomically: writes to `path.tmp` then renames to `path`.
460    pub fn save(&self, path: impl AsRef<Path>) -> Result<(), MqdbError> {
461        let path = path.as_ref();
462        let tmp_path = PathBuf::from(format!("{}.tmp", path.to_string_lossy()));
463        if tmp_path.exists() {
464            std::fs::remove_file(&tmp_path)?;
465        }
466
467        let write_result = (|| -> Result<(), MqdbError> {
468            let mut storage = Storage::create(&tmp_path)?;
469            let mut entries = Vec::with_capacity(self.documents.len());
470
471            // Phase 1: write block data
472            for doc in &self.documents {
473                let first_block_page = storage.write_document(doc)?;
474                entries.push(CatalogEntry {
475                    document_id: doc.id,
476                    path: doc.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
477                    first_block_page,
478                    num_blocks: doc.block_count,
479                    zone_map_bytes: encode_zone_map(&doc.zone_maps),
480                    index_start_page: 0,
481                });
482            }
483
484            // Phase 2: write secondary indexes
485            for (i, doc) in self.documents.iter().enumerate() {
486                let idx = if let Some(cached) = self.doc_indexes.get(i).and_then(|o| o.as_ref()) {
487                    std::borrow::Cow::Borrowed(cached)
488                } else {
489                    std::borrow::Cow::Owned(DocumentIndex::build(&doc.blocks))
490                };
491                let bytes = idx.to_bytes();
492                entries[i].index_start_page = storage.write_index(&bytes)?;
493            }
494
495            // This writes into a brand-new file, so each table's rows are
496            // written fresh here rather than reusing `first_row_page` /
497            // `last_row_page` from `self`, which (if set) point into a
498            // *different*, already-open backing file.
499            let ct_guard = self.custom_tables.read().unwrap();
500            let mut custom = Vec::with_capacity(ct_guard.len());
501            for (name, state) in ct_guard.iter() {
502                let (first_row_page, last_row_page) = storage.write_table_rows(&state.rows)?;
503                custom.push(CustomTableEntry {
504                    name: name.clone(),
505                    columns: state.columns.clone(),
506                    first_row_page,
507                    last_row_page,
508                    num_rows: state.rows.len() as u32,
509                });
510            }
511            drop(ct_guard);
512
513            storage.flush_catalog(&entries, &custom)?;
514            Ok(())
515        })();
516
517        if let Err(err) = write_result {
518            let _ = std::fs::remove_file(&tmp_path);
519            return Err(err);
520        }
521
522        std::fs::rename(&tmp_path, path)?;
523        Ok(())
524    }
525
526    /// Open a `.mq-db` file in lazy mode: reads only catalog and zone maps.
527    ///
528    /// Block data is not loaded until you call
529    /// [`load_all_blocks`](DocumentStore::load_all_blocks).  Secondary indexes
530    /// are not built until you call
531    /// [`load_all_indexes`](DocumentStore::load_all_indexes).
532    pub fn open(path: impl AsRef<Path>) -> Result<Self, MqdbError> {
533        let mut storage = Storage::open(path.as_ref())?;
534        let (entries, custom_table_entries) = storage.load_catalog()?;
535        let cap = entries.len();
536        let mut documents = Vec::with_capacity(cap);
537        let mut max_doc_id = None;
538
539        for entry in entries {
540            let zone_maps = decode_zone_map(&entry.zone_map_bytes)?;
541            let document_id = entry.document_id;
542            let path = entry.path.map(PathBuf::from);
543            documents.push(Document::from_catalog_lazy(
544                document_id,
545                path,
546                entry.num_blocks,
547                zone_maps,
548                entry.first_block_page,
549                entry.index_start_page,
550            ));
551            max_doc_id =
552                Some(max_doc_id.map_or(document_id, |cur: DocumentId| cur.max(document_id)));
553        }
554
555        let mut custom_tables = HashMap::new();
556        for ct in custom_table_entries {
557            let rows = storage.read_table_rows(ct.first_row_page, ct.num_rows, ct.columns.len())?;
558            custom_tables.insert(
559                ct.name,
560                CustomTableState {
561                    columns: ct.columns,
562                    rows,
563                    first_row_page: ct.first_row_page,
564                    last_row_page: ct.last_row_page,
565                },
566            );
567        }
568
569        Ok(Self {
570            documents,
571            next_doc_id: max_doc_id.map_or(0, |id| id.saturating_add(1)),
572            store_spans: true,
573            storage: Mutex::new(Some(storage)),
574            doc_indexes: vec![None; cap],
575            custom_tables: RwLock::new(custom_tables),
576        })
577    }
578
579    /// Load a `.mq-db` file and reconstruct the in-memory `DocumentStore`.
580    ///
581    /// All block data is read from disk. Secondary indexes are **not** built
582    /// here — [`crate::SqlEngine`] builds them lazily on construction.
583    pub fn load(path: impl AsRef<Path>) -> Result<Self, MqdbError> {
584        let mut storage = Storage::open(path.as_ref())?;
585        let (entries, custom_table_entries) = storage.load_catalog()?;
586        let cap = entries.len();
587        let mut documents = Vec::with_capacity(cap);
588        let mut max_doc_id = None;
589
590        for entry in entries {
591            let blocks = storage.read_blocks(entry.first_block_page, entry.num_blocks)?;
592            let zone_maps = decode_zone_map(&entry.zone_map_bytes)?;
593            let document_id = entry.document_id;
594            let path = entry.path.map(PathBuf::from);
595            let mut doc = Document::from_parts(document_id, path, blocks, zone_maps);
596            doc.index_start_page = entry.index_start_page;
597            documents.push(doc);
598            max_doc_id =
599                Some(max_doc_id.map_or(document_id, |cur: DocumentId| cur.max(document_id)));
600        }
601
602        let mut custom_tables = HashMap::new();
603        for ct in custom_table_entries {
604            let rows = storage.read_table_rows(ct.first_row_page, ct.num_rows, ct.columns.len())?;
605            custom_tables.insert(
606                ct.name,
607                CustomTableState {
608                    columns: ct.columns,
609                    rows,
610                    first_row_page: ct.first_row_page,
611                    last_row_page: ct.last_row_page,
612                },
613            );
614        }
615
616        Ok(Self {
617            documents,
618            next_doc_id: max_doc_id.map_or(0, |id| id.saturating_add(1)),
619            store_spans: true,
620            storage: Mutex::new(None),
621            doc_indexes: vec![None; cap],
622            custom_tables: RwLock::new(custom_tables),
623        })
624    }
625
626    /// Load only the catalog metadata from a `.mq-db` file — no block data.
627    ///
628    /// Documents have `block_count` populated from the catalog but `blocks`
629    /// is empty. Useful for commands that only need zone-map metadata (e.g.
630    /// `list`), avoiding the cost of deserialising all block data.
631    pub fn load_catalog_only(path: impl AsRef<Path>) -> Result<Self, MqdbError> {
632        let mut storage = Storage::open(path.as_ref())?;
633        let (entries, _custom_table_entries) = storage.load_catalog()?;
634        let cap = entries.len();
635        let mut documents = Vec::with_capacity(cap);
636        let mut max_doc_id = None;
637
638        for entry in entries {
639            let zone_maps = decode_zone_map(&entry.zone_map_bytes)?;
640            let document_id = entry.document_id;
641            let path = entry.path.map(PathBuf::from);
642            documents.push(Document::from_catalog(
643                document_id,
644                path,
645                entry.num_blocks,
646                zone_maps,
647            ));
648            max_doc_id =
649                Some(max_doc_id.map_or(document_id, |cur: DocumentId| cur.max(document_id)));
650        }
651
652        Ok(Self {
653            documents,
654            next_doc_id: max_doc_id.map_or(0, |id| id.saturating_add(1)),
655            store_spans: true,
656            storage: Mutex::new(None),
657            doc_indexes: vec![None; cap],
658            custom_tables: RwLock::new(HashMap::new()),
659        })
660    }
661}