stack_graphs/
storage.rs

1// -*- coding: utf-8 -*-
2// ------------------------------------------------------------------------------------------------
3// Copyright © 2023, stack-graphs authors.
4// Licensed under either of Apache License, Version 2.0, or MIT license, at your option.
5// Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details.
6// ------------------------------------------------------------------------------------------------
7
8use bincode::error::DecodeError;
9use bincode::error::EncodeError;
10use itertools::Itertools;
11use rusqlite::functions::FunctionFlags;
12use rusqlite::types::ValueRef;
13use rusqlite::Connection;
14use rusqlite::OptionalExtension;
15use rusqlite::Params;
16use rusqlite::Statement;
17use std::collections::HashSet;
18use std::path::Path;
19use std::path::PathBuf;
20use thiserror::Error;
21
22use crate::arena::Handle;
23use crate::graph::Degree;
24use crate::graph::File;
25use crate::graph::Node;
26use crate::graph::StackGraph;
27use crate::partial::PartialPath;
28use crate::partial::PartialPaths;
29use crate::partial::PartialSymbolStack;
30use crate::serde;
31use crate::serde::FileFilter;
32use crate::stitching::Database;
33use crate::stitching::ForwardCandidates;
34use crate::CancellationError;
35use crate::CancellationFlag;
36
37const VERSION: usize = 6;
38
39const SCHEMA: &str = r#"
40        CREATE TABLE metadata (
41            version INTEGER NOT NULL
42        ) STRICT;
43        CREATE TABLE graphs (
44            file   TEXT PRIMARY KEY,
45            tag    TEXT NOT NULL,
46            error  TEXT,
47            value  BLOB NOT NULL
48        ) STRICT;
49        CREATE TABLE file_paths (
50            file     TEXT NOT NULL,
51            local_id INTEGER NOT NULL,
52            value    BLOB NOT NULL,
53            FOREIGN KEY(file) REFERENCES graphs(file)
54        ) STRICT;
55        CREATE TABLE root_paths (
56            file         TEXT NOT NULL,
57            symbol_stack TEXT NOT NULL,
58            value        BLOB NOT NULL,
59            FOREIGN KEY(file) REFERENCES graphs(file)
60        ) STRICT;
61    "#;
62
63const INDEXES: &str = r#"
64        CREATE INDEX IF NOT EXISTS idx_graphs_file ON graphs(file);
65        CREATE INDEX IF NOT EXISTS idx_file_paths_local_id ON file_paths(file, local_id);
66        CREATE INDEX IF NOT EXISTS idx_root_paths_symbol_stack ON root_paths(symbol_stack);
67    "#;
68
69const PRAGMAS: &str = r#"
70        PRAGMA journal_mode = WAL;
71        PRAGMA foreign_keys = false;
72        PRAGMA secure_delete = false;
73    "#;
74
75pub static BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
76
77#[derive(Debug, Error)]
78pub enum StorageError {
79    #[error("cancelled at {0}")]
80    Cancelled(&'static str),
81    #[error("unsupported database version {0}")]
82    IncorrectVersion(usize),
83    #[error("database does not exist {0}")]
84    MissingDatabase(String),
85    #[error(transparent)]
86    Rusqlite(#[from] rusqlite::Error),
87    #[error(transparent)]
88    Serde(#[from] serde::Error),
89    #[error(transparent)]
90    SerializeFail(#[from] EncodeError),
91    #[error(transparent)]
92    DeserializeFail(#[from] DecodeError),
93}
94
95pub type Result<T> = std::result::Result<T, StorageError>;
96
97impl From<CancellationError> for StorageError {
98    fn from(value: CancellationError) -> Self {
99        Self::Cancelled(value.0)
100    }
101}
102
103/// The status of a file in the database.
104pub enum FileStatus {
105    Missing,
106    Indexed,
107    Error(String),
108}
109
110impl<'a> From<ValueRef<'a>> for FileStatus {
111    fn from(value: ValueRef<'a>) -> Self {
112        match value {
113            ValueRef::Null => Self::Indexed,
114            ValueRef::Text(error) => Self::Error(
115                std::str::from_utf8(error)
116                    .expect("invalid error encoding in database")
117                    .to_string(),
118            ),
119            _ => panic!("invalid value type in database"),
120        }
121    }
122}
123
124/// A file entry in the database.
125pub struct FileEntry {
126    pub path: PathBuf,
127    pub tag: String,
128    pub status: FileStatus,
129}
130
131/// An iterator over a query returning rows with (path,tag,error) tuples.
132pub struct Files<'a, P: Params>(Statement<'a>, P);
133
134impl<'a, P: Params + Clone> Files<'a, P> {
135    pub fn try_iter<'b>(&'b mut self) -> Result<impl Iterator<Item = Result<FileEntry>> + 'b> {
136        let entries = self.0.query_map(self.1.clone(), |r| {
137            Ok(FileEntry {
138                path: PathBuf::from(r.get::<_, String>(0)?),
139                tag: r.get::<_, String>(1)?,
140                status: r.get_ref(2)?.into(),
141            })
142        })?;
143        let entries = entries.map(|r| -> Result<FileEntry> { Ok(r?) });
144        Ok(entries)
145    }
146}
147
148/// Writer to store stack graphs and partial paths in a SQLite database.
149pub struct SQLiteWriter {
150    conn: Connection,
151}
152
153impl SQLiteWriter {
154    /// Open an in-memory database.
155    pub fn open_in_memory() -> Result<Self> {
156        let mut conn = Connection::open_in_memory()?;
157        Self::init(&mut conn)?;
158        init_indexes(&mut conn)?;
159        Ok(Self { conn })
160    }
161
162    /// Open a file database.  If the file does not exist, it is automatically created.
163    /// An error is returned if the database version is not supported.
164    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
165        let is_new = !path.as_ref().exists();
166        let mut conn = Connection::open(path)?;
167        set_pragmas_and_functions(&conn)?;
168        if is_new {
169            Self::init(&mut conn)?;
170        } else {
171            check_version(&conn)?;
172        }
173        init_indexes(&mut conn)?;
174        Ok(Self { conn })
175    }
176
177    /// Create database tables and write metadata.
178    fn init(conn: &mut Connection) -> Result<()> {
179        let tx = conn.transaction()?;
180        tx.execute_batch(SCHEMA)?;
181        tx.execute("INSERT INTO metadata (version) VALUES (?)", [VERSION])?;
182        tx.commit()?;
183        Ok(())
184    }
185
186    /// Clean all data from the database.
187    pub fn clean_all(&mut self) -> Result<usize> {
188        let tx = self.conn.transaction()?;
189        let count = Self::clean_all_inner(&tx)?;
190        tx.commit()?;
191        Ok(count)
192    }
193
194    /// Clean all data from the database.
195    ///
196    /// This is an inner method, which does not wrap individual SQL statements in a transaction.
197    fn clean_all_inner(conn: &Connection) -> Result<usize> {
198        {
199            let mut stmt = conn.prepare_cached("DELETE FROM file_paths")?;
200            stmt.execute([])?;
201        }
202        {
203            let mut stmt = conn.prepare_cached("DELETE FROM root_paths")?;
204            stmt.execute([])?;
205        }
206        let count = {
207            let mut stmt = conn.prepare_cached("DELETE FROM graphs")?;
208            stmt.execute([])?
209        };
210        Ok(count)
211    }
212
213    /// Clean file data from the database.  If recursive is true, data for all descendants of
214    /// that file is cleaned.
215    pub fn clean_file(&mut self, file: &Path) -> Result<usize> {
216        let tx = self.conn.transaction()?;
217        let count = Self::clean_file_inner(&tx, file)?;
218        tx.commit()?;
219        Ok(count)
220    }
221
222    /// Clean file data from the database.
223    ///
224    /// This is an inner method, which does not wrap individual SQL statements in a transaction.
225    fn clean_file_inner(conn: &Connection, file: &Path) -> Result<usize> {
226        let file = file.to_string_lossy();
227        {
228            let mut stmt = conn.prepare_cached("DELETE FROM file_paths WHERE file=?")?;
229            stmt.execute([&file])?;
230        }
231        {
232            let mut stmt = conn.prepare_cached("DELETE FROM root_paths WHERE file=?")?;
233            stmt.execute([&file])?;
234        }
235        let count = {
236            let mut stmt = conn.prepare_cached("DELETE FROM graphs WHERE file=?")?;
237            stmt.execute([&file])?
238        };
239        Ok(count)
240    }
241
242    /// Clean file or directory data from the database.  Data for all decendants of the given path
243    /// is cleaned.
244    pub fn clean_file_or_directory(&mut self, file_or_directory: &Path) -> Result<usize> {
245        let tx = self.conn.transaction()?;
246        let count = Self::clean_file_or_directory_inner(&tx, file_or_directory)?;
247        tx.commit()?;
248        Ok(count)
249    }
250
251    /// Clean file or directory data from the database.  Data for all decendants of the given path
252    /// is cleaned.
253    ///
254    /// This is an inner method, which does not wrap individual SQL statements in a transaction.
255    fn clean_file_or_directory_inner(conn: &Connection, file_or_directory: &Path) -> Result<usize> {
256        let file_or_directory = file_or_directory.to_string_lossy();
257        {
258            let mut stmt =
259                conn.prepare_cached("DELETE FROM file_paths WHERE path_descendant_of(file, ?)")?;
260            stmt.execute([&file_or_directory])?;
261        }
262        {
263            let mut stmt =
264                conn.prepare_cached("DELETE FROM root_paths WHERE path_descendant_of(file, ?)")?;
265            stmt.execute([&file_or_directory])?;
266        }
267        let count = {
268            let mut stmt =
269                conn.prepare_cached("DELETE FROM graphs WHERE path_descendant_of(file, ?)")?;
270            stmt.execute([&file_or_directory])?
271        };
272        Ok(count)
273    }
274
275    /// Store an error, indicating that indexing this file failed.
276    pub fn store_error_for_file(&mut self, file: &Path, tag: &str, error: &str) -> Result<()> {
277        let tx = self.conn.transaction()?;
278        Self::store_error_for_file_inner(&tx, file, tag, error)?;
279        tx.commit()?;
280        Ok(())
281    }
282
283    /// Store an error, indicating that indexing this file failed.
284    ///
285    /// This is an inner method, which does not wrap individual SQL statements in a transaction.
286    fn store_error_for_file_inner(
287        conn: &Connection,
288        file: &Path,
289        tag: &str,
290        error: &str,
291    ) -> Result<()> {
292        copious_debugging!("--> Store error for {}", file.display());
293        let mut stmt = conn
294            .prepare_cached("INSERT INTO graphs (file, tag, error, value) VALUES (?, ?, ?, ?)")?;
295        let graph = crate::serde::StackGraph::default();
296        let serialized = bincode::encode_to_vec(&graph, BINCODE_CONFIG)?;
297        stmt.execute((&file.to_string_lossy(), tag, error, serialized))?;
298        Ok(())
299    }
300
301    /// Store the result of a successful file index.
302    pub fn store_result_for_file<'a, IP>(
303        &mut self,
304        graph: &StackGraph,
305        file: Handle<File>,
306        tag: &str,
307        partials: &mut PartialPaths,
308        paths: IP,
309    ) -> Result<()>
310    where
311        IP: IntoIterator<Item = &'a PartialPath>,
312    {
313        let path = Path::new(graph[file].name());
314        let tx = self.conn.transaction()?;
315        Self::clean_file_inner(&tx, path)?;
316        Self::store_graph_for_file_inner(&tx, graph, file, tag)?;
317        Self::store_partial_paths_for_file_inner(&tx, graph, file, partials, paths)?;
318        tx.commit()?;
319        Ok(())
320    }
321
322    /// Store the file graph.
323    ///
324    /// This is an inner method, which does not wrap individual SQL statements in a transaction.
325    fn store_graph_for_file_inner(
326        conn: &Connection,
327        graph: &StackGraph,
328        file: Handle<File>,
329        tag: &str,
330    ) -> Result<()> {
331        let file_str = graph[file].name();
332        copious_debugging!("--> Store graph for {}", file_str);
333        let mut stmt =
334            conn.prepare_cached("INSERT INTO graphs (file, tag, value) VALUES (?, ?, ?)")?;
335        let graph = serde::StackGraph::from_graph_filter(graph, &FileFilter(file));
336        let serialized = bincode::encode_to_vec(&graph, BINCODE_CONFIG)?;
337        stmt.execute((file_str, tag, &serialized))?;
338        Ok(())
339    }
340
341    /// Store the file partial paths.
342    ///
343    /// This is an inner method, which does not wrap individual SQL statements in a transaction.
344    fn store_partial_paths_for_file_inner<'a, IP>(
345        conn: &Connection,
346        graph: &StackGraph,
347        file: Handle<File>,
348        partials: &mut PartialPaths,
349        paths: IP,
350    ) -> Result<()>
351    where
352        IP: IntoIterator<Item = &'a PartialPath>,
353    {
354        let file_str = graph[file].name();
355        let mut node_stmt =
356            conn.prepare_cached("INSERT INTO file_paths (file, local_id, value) VALUES (?, ?, ?)")?;
357        let mut root_stmt = conn.prepare_cached(
358            "INSERT INTO root_paths (file, symbol_stack, value) VALUES (?, ?, ?)",
359        )?;
360        #[cfg_attr(not(feature = "copious-debugging"), allow(unused))]
361        let mut node_path_count = 0usize;
362        #[cfg_attr(not(feature = "copious-debugging"), allow(unused))]
363        let mut root_path_count = 0usize;
364        for path in paths {
365            copious_debugging!(
366                "--> Add {} partial path {}",
367                file_str,
368                path.display(graph, partials)
369            );
370            let start_node = graph[path.start_node].id();
371            if start_node.is_root() {
372                copious_debugging!(
373                    " * Add as root path with symbol stack {}",
374                    path.symbol_stack_precondition.display(graph, partials),
375                );
376                let symbol_stack = path.symbol_stack_precondition.storage_key(graph, partials);
377                let path = serde::PartialPath::from_partial_path(graph, partials, path);
378                let serialized = bincode::encode_to_vec(&path, BINCODE_CONFIG)?;
379                root_stmt.execute((file_str, symbol_stack, serialized))?;
380                root_path_count += 1;
381            } else if start_node.is_in_file(file) {
382                copious_debugging!(
383                    " * Add as node path from node {}",
384                    path.start_node.display(graph),
385                );
386                let path = serde::PartialPath::from_partial_path(graph, partials, path);
387                let serialized = bincode::encode_to_vec(&path, BINCODE_CONFIG)?;
388                node_stmt.execute((file_str, path.start_node.local_id, serialized))?;
389                node_path_count += 1;
390            } else {
391                panic!(
392                    "added path {} must start in given file {} or at root",
393                    path.display(graph, partials),
394                    graph[file].name()
395                );
396            }
397            copious_debugging!(
398                " * Added {} node paths and {} root paths",
399                node_path_count,
400                root_path_count,
401            );
402        }
403        Ok(())
404    }
405
406    /// Get the file's status in the database. If a tag is provided, it must match or the file
407    /// is reported missing.
408    pub fn status_for_file(&mut self, file: &str, tag: Option<&str>) -> Result<FileStatus> {
409        status_for_file(&self.conn, file, tag)
410    }
411
412    /// Convert this writer into a reader for the same database.
413    pub fn into_reader(self) -> SQLiteReader {
414        SQLiteReader {
415            conn: self.conn,
416            loaded_graphs: HashSet::new(),
417            loaded_node_paths: HashSet::new(),
418            loaded_root_paths: HashSet::new(),
419            graph: StackGraph::new(),
420            partials: PartialPaths::new(),
421            db: Database::new(),
422            stats: Stats::default(),
423        }
424    }
425}
426
427/// Reader to load stack graphs and partial paths from a SQLite database.
428pub struct SQLiteReader {
429    conn: Connection,
430    loaded_graphs: HashSet<String>,
431    loaded_node_paths: HashSet<Handle<Node>>,
432    loaded_root_paths: HashSet<String>,
433    graph: StackGraph,
434    partials: PartialPaths,
435    db: Database,
436    stats: Stats,
437}
438
439impl SQLiteReader {
440    /// Open a file database.
441    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
442        if !path.as_ref().exists() {
443            return Err(StorageError::MissingDatabase(
444                path.as_ref().to_string_lossy().to_string(),
445            ));
446        }
447        let mut conn = Connection::open(path)?;
448        set_pragmas_and_functions(&conn)?;
449        check_version(&conn)?;
450        init_indexes(&mut conn)?;
451        Ok(Self {
452            conn,
453            loaded_graphs: HashSet::new(),
454            loaded_node_paths: HashSet::new(),
455            loaded_root_paths: HashSet::new(),
456            graph: StackGraph::new(),
457            partials: PartialPaths::new(),
458            db: Database::new(),
459            stats: Stats::default(),
460        })
461    }
462
463    /// Clear all data that has been loaded into this reader instance.
464    /// After this call, all existing handles from this reader are invalid.
465    pub fn clear(&mut self) {
466        self.loaded_graphs.clear();
467        self.graph = StackGraph::new();
468
469        self.loaded_node_paths.clear();
470        self.loaded_root_paths.clear();
471        self.partials.clear();
472        self.db.clear();
473
474        self.stats.clear();
475    }
476
477    /// Clear path data that has been loaded into this reader instance.
478    /// After this call, all node handles remain valid, but all path data
479    /// is invalid.
480    pub fn clear_paths(&mut self) {
481        self.loaded_node_paths.clear();
482        self.loaded_root_paths.clear();
483        self.partials.clear();
484        self.db.clear();
485
486        self.stats.clear_paths();
487    }
488
489    /// Get the file's status in the database. If a tag is provided, it must match or the file
490    /// is reported missing.
491    pub fn status_for_file<T: AsRef<str>>(
492        &mut self,
493        file: &str,
494        tag: Option<T>,
495    ) -> Result<FileStatus> {
496        status_for_file(&self.conn, file, tag)
497    }
498
499    /// Returns a [`Files`][] value that can be used to iterate over all files in the database.
500    pub fn list_all<'a>(&'a mut self) -> Result<Files<'a, ()>> {
501        self.conn
502            .prepare("SELECT file, tag, error FROM graphs")
503            .map(|stmt| Files(stmt, ()))
504            .map_err(|e| e.into())
505    }
506
507    /// Returns a [`Files`][] value that can be used to iterate over all descendants of a
508    /// file or directory in the database.
509    pub fn list_file_or_directory<'a>(
510        &'a self,
511        file_or_directory: &Path,
512    ) -> Result<Files<'a, [String; 1]>> {
513        Self::list_file_or_directory_inner(&self.conn, file_or_directory)
514    }
515
516    fn list_file_or_directory_inner<'a>(
517        conn: &'a Connection,
518        file_or_directory: &Path,
519    ) -> Result<Files<'a, [String; 1]>> {
520        let file_or_directory = file_or_directory.to_string_lossy().to_string();
521        conn.prepare("SELECT file, tag, error FROM graphs WHERE path_descendant_of(file, ?)")
522            .map(|stmt| Files(stmt, [file_or_directory]))
523            .map_err(|e| e.into())
524    }
525
526    /// Ensure the graph for the given file is loaded.
527    pub fn load_graph_for_file(&mut self, file: &str) -> Result<Handle<File>> {
528        Self::load_graph_for_file_inner(
529            file,
530            &mut self.graph,
531            &mut self.loaded_graphs,
532            &self.conn,
533            &mut self.stats,
534        )
535    }
536
537    fn load_graph_for_file_inner(
538        file: &str,
539        graph: &mut StackGraph,
540        loaded_graphs: &mut HashSet<String>,
541        conn: &Connection,
542        stats: &mut Stats,
543    ) -> Result<Handle<File>> {
544        copious_debugging!("--> Load graph for {}", file);
545        if !loaded_graphs.insert(file.to_string()) {
546            copious_debugging!(" * Already loaded");
547            stats.file_cached += 1;
548            return Ok(graph.get_file(file).expect("loaded file to exist"));
549        }
550        copious_debugging!(" * Load from database");
551        stats.file_loads += 1;
552        let mut stmt = conn.prepare_cached("SELECT value FROM graphs WHERE file = ?")?;
553        let value = stmt.query_row([file], |row| row.get::<_, Vec<u8>>(0))?;
554        let (file_graph, _): (serde::StackGraph, usize) =
555            bincode::decode_from_slice(&value, BINCODE_CONFIG)?;
556        file_graph.load_into(graph)?;
557        Ok(graph.get_file(file).expect("loaded file to exist"))
558    }
559
560    pub fn load_graphs_for_file_or_directory(
561        &mut self,
562        file_or_directory: &Path,
563        cancellation_flag: &dyn CancellationFlag,
564    ) -> Result<()> {
565        for file in Self::list_file_or_directory_inner(&self.conn, file_or_directory)?.try_iter()? {
566            cancellation_flag.check("loading graphs")?;
567            let file = file?;
568            Self::load_graph_for_file_inner(
569                &file.path.to_string_lossy(),
570                &mut self.graph,
571                &mut self.loaded_graphs,
572                &self.conn,
573                &mut self.stats,
574            )?;
575        }
576        Ok(())
577    }
578
579    /// Ensure the paths starting a the given node are loaded.
580    fn load_paths_for_node(
581        &mut self,
582        node: Handle<Node>,
583        cancellation_flag: &dyn CancellationFlag,
584    ) -> Result<()> {
585        copious_debugging!(" * Load extensions from node {}", node.display(&self.graph));
586        if !self.loaded_node_paths.insert(node) {
587            copious_debugging!("   > Already loaded");
588            self.stats.node_path_cached += 1;
589            return Ok(());
590        }
591        self.stats.node_path_loads += 1;
592        let id = self.graph[node].id();
593        let file = id.file().expect("file node required");
594        let file = self.graph[file].name();
595        let mut stmt = self
596            .conn
597            .prepare_cached("SELECT file,value from file_paths WHERE file = ? AND local_id = ?")?;
598        let paths = stmt.query_map((file, id.local_id()), |row| {
599            let file = row.get::<_, String>(0)?;
600            let value = row.get::<_, Vec<u8>>(1)?;
601            Ok((file, value))
602        })?;
603        #[cfg_attr(not(feature = "copious-debugging"), allow(unused))]
604        let mut count = 0usize;
605        for path in paths {
606            cancellation_flag.check("loading node paths")?;
607            let (file, value) = path?;
608            Self::load_graph_for_file_inner(
609                &file,
610                &mut self.graph,
611                &mut self.loaded_graphs,
612                &self.conn,
613                &mut self.stats,
614            )?;
615            let (path, _): (serde::PartialPath, usize) =
616                bincode::decode_from_slice(&value, BINCODE_CONFIG)?;
617            let path = path.to_partial_path(&mut self.graph, &mut self.partials)?;
618            copious_debugging!(
619                "   > Loaded {}",
620                path.display(&self.graph, &mut self.partials)
621            );
622            self.db
623                .add_partial_path(&self.graph, &mut self.partials, path);
624            count += 1;
625        }
626        copious_debugging!("   > Loaded {}", count);
627        Ok(())
628    }
629
630    /// Ensure the paths starting at the root and matching the given symbol stack are loaded.
631    fn load_paths_for_root(
632        &mut self,
633        symbol_stack: PartialSymbolStack,
634        cancellation_flag: &dyn CancellationFlag,
635    ) -> Result<()> {
636        copious_debugging!(
637            " * Load extensions from root with symbol stack {}",
638            symbol_stack.display(&self.graph, &mut self.partials)
639        );
640        let mut stmt = self.conn.prepare_cached(
641            "SELECT file,value from root_paths WHERE symbol_stack LIKE ? ESCAPE ?",
642        )?;
643        let (symbol_stack_patterns, escape) =
644            symbol_stack.storage_key_patterns(&self.graph, &mut self.partials);
645        for symbol_stack in symbol_stack_patterns {
646            copious_debugging!(
647                " * Load extensions from root with prefix symbol stack {}",
648                symbol_stack
649            );
650            if !self.loaded_root_paths.insert(symbol_stack.clone()) {
651                copious_debugging!("   > Already loaded");
652                self.stats.root_path_cached += 1;
653                continue;
654            }
655            self.stats.root_path_loads += 1;
656            let paths = stmt.query_map([symbol_stack, escape.clone()], |row| {
657                let file = row.get::<_, String>(0)?;
658                let value = row.get::<_, Vec<u8>>(1)?;
659                Ok((file, value))
660            })?;
661            #[cfg_attr(not(feature = "copious-debugging"), allow(unused))]
662            let mut count = 0usize;
663            for path in paths {
664                cancellation_flag.check("loading root paths")?;
665                let (file, value) = path?;
666                Self::load_graph_for_file_inner(
667                    &file,
668                    &mut self.graph,
669                    &mut self.loaded_graphs,
670                    &self.conn,
671                    &mut self.stats,
672                )?;
673                let (path, _): (serde::PartialPath, usize) =
674                    bincode::decode_from_slice(&value, BINCODE_CONFIG)?;
675                let path = path.to_partial_path(&mut self.graph, &mut self.partials)?;
676                copious_debugging!(
677                    "   > Loaded {}",
678                    path.display(&self.graph, &mut self.partials)
679                );
680                self.db
681                    .add_partial_path(&self.graph, &mut self.partials, path);
682                count += 1;
683            }
684            copious_debugging!("   > Loaded {}", count);
685        }
686        Ok(())
687    }
688
689    /// Ensure all possible extensions for the given partial path are loaded.
690    pub fn load_partial_path_extensions(
691        &mut self,
692        path: &PartialPath,
693        cancellation_flag: &dyn CancellationFlag,
694    ) -> Result<()> {
695        copious_debugging!(
696            "--> Load extensions for {}",
697            path.display(&self.graph, &mut self.partials)
698        );
699        let end_node = self.graph[path.end_node].id();
700        if self.graph[path.end_node].file().is_some() {
701            self.load_paths_for_node(path.end_node, cancellation_flag)?;
702        } else if end_node.is_root() {
703            self.load_paths_for_root(path.symbol_stack_postcondition, cancellation_flag)?;
704        }
705        Ok(())
706    }
707
708    /// Get the stack graph, partial paths arena, and path database for the currently loaded data.
709    pub fn get(&mut self) -> (&mut StackGraph, &mut PartialPaths, &mut Database) {
710        (&mut self.graph, &mut self.partials, &mut self.db)
711    }
712
713    /// Return stats about this database reader.
714    pub fn stats(&self) -> Stats {
715        self.stats.clone()
716    }
717}
718
719// Methods for computing keys and patterns for a symbol stack. The format of a storage key is:
720//
721//     has-var GS ( symbol (US symbol)* )?
722//
723// where has-var is "V" if the symbol stack has a variable, "X" otherwise.
724impl PartialSymbolStack {
725    /// Returns a string representation of this symbol stack for indexing in the database.
726    fn storage_key(self, graph: &StackGraph, partials: &mut PartialPaths) -> String {
727        let mut key = String::new();
728        match self.has_variable() {
729            true => key += "V\u{241E}",
730            false => key += "X\u{241E}",
731        }
732        key += &self
733            .iter(partials)
734            .map(|s| &graph[s.symbol])
735            .join("\u{241F}");
736        key
737    }
738
739    /// Returns string representations for all prefixes of this symbol stack for querying the
740    /// index in the database.
741    fn storage_key_patterns(
742        mut self,
743        graph: &StackGraph,
744        partials: &mut PartialPaths,
745    ) -> (Vec<String>, String) {
746        let mut key_patterns = Vec::new();
747        let mut symbols = String::new();
748        while let Some(symbol) = self.pop_front(partials) {
749            if !symbols.is_empty() {
750                symbols += "\u{241F}";
751            }
752            let symbol = graph[symbol.symbol]
753                .replace("%", "\\%")
754                .replace("_", "\\_")
755                .to_string();
756            symbols += &symbol;
757            // patterns for paths matching a prefix of this stack
758            key_patterns.push("V\u{241E}".to_string() + &symbols);
759        }
760        // pattern for paths matching exactly this stack
761        key_patterns.push("X\u{241E}".to_string() + &symbols);
762        if self.has_variable() {
763            // patterns for paths for which this stack is a prefix
764            key_patterns.push("_\u{241E}".to_string() + &symbols + "\u{241F}%");
765        }
766        (key_patterns, "\\".to_string())
767    }
768}
769
770impl ForwardCandidates<Handle<PartialPath>, PartialPath, Database, StorageError> for SQLiteReader {
771    fn load_forward_candidates(
772        &mut self,
773        path: &PartialPath,
774        cancellation_flag: &dyn CancellationFlag,
775    ) -> std::result::Result<(), StorageError> {
776        self.load_partial_path_extensions(path, cancellation_flag)
777    }
778
779    fn get_forward_candidates<R>(&mut self, path: &PartialPath, result: &mut R)
780    where
781        R: std::iter::Extend<Handle<PartialPath>>,
782    {
783        self.db
784            .find_candidate_partial_paths(&self.graph, &mut self.partials, path, result);
785    }
786
787    fn get_joining_candidate_degree(&self, path: &PartialPath) -> Degree {
788        self.db.get_incoming_path_degree(path.end_node)
789    }
790
791    fn get_graph_partials_and_db(&mut self) -> (&StackGraph, &mut PartialPaths, &Database) {
792        (&self.graph, &mut self.partials, &self.db)
793    }
794}
795
796#[derive(Clone, Debug, Default)]
797pub struct Stats {
798    pub file_loads: usize,
799    pub file_cached: usize,
800    pub root_path_loads: usize,
801    pub root_path_cached: usize,
802    pub node_path_loads: usize,
803    pub node_path_cached: usize,
804}
805
806impl Stats {
807    fn clear(&mut self) {
808        *self = Stats::default();
809    }
810
811    fn clear_paths(&mut self) {
812        *self = Stats {
813            file_loads: self.file_loads,
814            file_cached: self.file_cached,
815            ..Stats::default()
816        }
817    }
818}
819
820/// Check if the database has the version supported by this library version.
821fn check_version(conn: &Connection) -> Result<()> {
822    let version = conn.query_row("SELECT version FROM metadata", [], |r| r.get::<_, usize>(0))?;
823    if version != VERSION {
824        return Err(StorageError::IncorrectVersion(version));
825    }
826    Ok(())
827}
828
829fn set_pragmas_and_functions(conn: &Connection) -> Result<()> {
830    conn.execute_batch(PRAGMAS)?;
831    conn.create_scalar_function(
832        "path_descendant_of",
833        2,
834        FunctionFlags::SQLITE_DETERMINISTIC | FunctionFlags::SQLITE_UTF8,
835        move |ctx| {
836            assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
837            let path = PathBuf::from(ctx.get::<String>(0)?);
838            let parent = PathBuf::from(ctx.get::<String>(1)?);
839            let result = path.starts_with(&parent);
840            Ok(result)
841        },
842    )?;
843    Ok(())
844}
845
846fn init_indexes(conn: &mut Connection) -> Result<()> {
847    let tx = conn.transaction()?;
848    tx.execute_batch(INDEXES)?;
849    tx.commit()?;
850    Ok(())
851}
852
853fn status_for_file<T: AsRef<str>>(
854    conn: &Connection,
855    file: &str,
856    tag: Option<T>,
857) -> Result<FileStatus> {
858    let result = if let Some(tag) = tag {
859        let mut stmt =
860            conn.prepare_cached("SELECT error FROM graphs WHERE file = ? AND tag = ?")?;
861        stmt.query_row([file, tag.as_ref()], |r| r.get_ref(0).map(FileStatus::from))
862            .optional()?
863            .unwrap_or(FileStatus::Missing)
864    } else {
865        let mut stmt = conn.prepare_cached("SELECT status FROM graphs WHERE file = ?")?;
866        stmt.query_row([file], |r| r.get_ref(0).map(FileStatus::from))
867            .optional()?
868            .unwrap_or(FileStatus::Missing)
869    };
870    Ok(result)
871}