Skip to main content

pelicode_db/
stats.rs

1//! Document frequencies of hashes: how many packages contain each exact hash
2//! and fingerprint. Hashes found in many packages (getters, `equals`,
3//! license headers turned into code, ...) are no evidence of copying.
4
5use pelicode_core::{Hash128, Level};
6use rusqlite::{Connection, OptionalExtension, params};
7use serde::Serialize;
8
9use crate::{Database, Result};
10
11/// State of the statistics.
12#[derive(Debug, Clone, Serialize)]
13pub struct HashStatsInfo {
14    /// Packages with at least one indexed file.
15    pub packages: usize,
16    /// When the statistics were last rebuilt.
17    pub updated_at: Option<String>,
18    /// Whether something was indexed after the last rebuild (or never rebuilt).
19    pub stale: bool,
20    /// Exact hashes found in two or more packages.
21    pub common_exact_hashes: usize,
22    /// Fingerprints found in two or more packages.
23    pub common_fingerprints: usize,
24    /// Whether [`Database::refresh_hash_stats`] can recount only the hashes of
25    /// what was indexed since the last statistics. Not before the first
26    /// statistics, nor for a database upgraded while they were out of date.
27    pub incremental: bool,
28    /// Hashes waiting to be recounted by an incremental refresh.
29    pub pending_hashes: usize,
30}
31
32/// A hash shared by several packages, with one function that has it.
33#[derive(Debug, Clone, Serialize)]
34pub struct CommonHash {
35    /// Normalization level of the hash.
36    pub level: Level,
37    /// The hash (hex).
38    pub hash: String,
39    /// Packages containing it.
40    pub package_count: usize,
41    /// Functions containing it.
42    pub function_count: usize,
43    /// `ecosystem:package@version path:start-end name` of one function with the hash.
44    pub sample: Option<String>,
45}
46
47impl Database {
48    /// Brings the document frequencies up to date with the functions currently
49    /// linked to packages: incrementally when possible (see
50    /// [`HashStatsInfo::incremental`]), otherwise by recounting everything.
51    /// Both give the same counts.
52    ///
53    /// # Errors
54    ///
55    /// [`Error`](crate::Error) when the database cannot be read or written.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// # use pelicode_core::{Config, Language, extract_bytes};
61    /// # use pelicode_db::{Database, PackageInfo};
62    /// # let dir = tempfile::tempdir().unwrap();
63    /// # let config = Config { min_tokens: 0, ..Config::default() };
64    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
65    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
66    /// # let file = extract_bytes("calc.py".as_ref(), Language::Python, source, &config).unwrap();
67    /// # let package = |name: &str| PackageInfo {
68    /// #     ecosystem: "pypi".into(), name: name.into(), version: "1.0".into(), ..PackageInfo::default()
69    /// # };
70    /// db.index(&package("calc"), &[file.clone()]).unwrap();
71    /// assert!(db.hash_stats_info().unwrap().stale);
72    ///
73    /// // The first statistics are counted from scratch.
74    /// assert!(!db.hash_stats_info().unwrap().incremental);
75    /// let info = db.refresh_hash_stats().unwrap();
76    /// assert!(!info.stale);
77    /// assert_eq!(info.common_exact_hashes, 0);
78    ///
79    /// // Later ones recount only the hashes of what was indexed since.
80    /// db.index(&package("copy"), &[file]).unwrap();
81    /// assert!(db.hash_stats_info().unwrap().incremental);
82    /// let info = db.refresh_hash_stats().unwrap();
83    /// assert_eq!(info.packages, 2);
84    /// assert!(info.common_exact_hashes > 0);
85    /// ```
86    pub fn refresh_hash_stats(&mut self) -> Result<HashStatsInfo> {
87        if self.hash_stats_info()?.incremental {
88            self.refresh_pending_hash_stats()
89        } else {
90            self.rebuild_hash_stats()
91        }
92    }
93
94    /// Recounts the document frequencies of every hash, whatever was indexed
95    /// since the last statistics.
96    ///
97    /// # Errors
98    ///
99    /// [`Error`](crate::Error) when the database cannot be read or written.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use pelicode_core::Config;
105    /// use pelicode_db::Database;
106    ///
107    /// let dir = tempfile::tempdir().unwrap();
108    /// let mut db = Database::open(dir.path().join("pelicode.db"), &Config::default()).unwrap();
109    /// let info = db.rebuild_hash_stats().unwrap();
110    /// assert!(info.updated_at.is_some());
111    /// ```
112    pub fn rebuild_hash_stats(&mut self) -> Result<HashStatsInfo> {
113        let tx = self.conn.transaction()?;
114        tx.execute_batch(
115            "DROP TABLE IF EXISTS temp.function_package;
116             CREATE TEMP TABLE function_package AS
117                 SELECT DISTINCT f.id AS function_id, pv.package_id AS package_id
118                 FROM function f
119                 JOIN version_file vf ON vf.source_file_id = f.source_file_id
120                 JOIN package_version pv ON pv.id = vf.package_version_id;
121             CREATE INDEX temp.function_package_function ON function_package(function_id);
122
123             DELETE FROM exact_stats;
124             INSERT INTO exact_stats (level, hash, package_count, function_count)
125                 SELECT e.level, e.hash, COUNT(DISTINCT fp.package_id), COUNT(DISTINCT e.function_id)
126                 FROM exact_hash e JOIN function_package fp ON fp.function_id = e.function_id
127                 GROUP BY e.level, e.hash
128                 HAVING COUNT(DISTINCT fp.package_id) >= 2;
129
130             DELETE FROM fingerprint_stats;
131             INSERT INTO fingerprint_stats (level, hash, package_count, function_count)
132                 SELECT f.level, f.hash, COUNT(DISTINCT fp.package_id), COUNT(DISTINCT f.function_id)
133                 FROM fingerprint f JOIN function_package fp ON fp.function_id = f.function_id
134                 GROUP BY f.level, f.hash
135                 HAVING COUNT(DISTINCT fp.package_id) >= 2;
136
137             DROP TABLE temp.function_package;",
138        )?;
139        finish_refresh(&tx)?;
140        tx.commit()?;
141        self.hash_stats_info()
142    }
143
144    /// Recounts only the hashes of the files linked to or unlinked from a
145    /// version since the last statistics; the counts of the other hashes
146    /// cannot have changed.
147    fn refresh_pending_hash_stats(&mut self) -> Result<HashStatsInfo> {
148        let tx = self.conn.transaction()?;
149        tx.execute_batch(
150            "DELETE FROM exact_stats
151                 WHERE (level, hash) IN (SELECT level, hash FROM stats_pending_exact);
152             INSERT INTO exact_stats (level, hash, package_count, function_count)
153                 SELECT e.level, e.hash, COUNT(DISTINCT pv.package_id), COUNT(DISTINCT e.function_id)
154                 FROM stats_pending_exact p
155                 JOIN exact_hash e ON e.level = p.level AND e.hash = p.hash
156                 JOIN function f ON f.id = e.function_id
157                 JOIN version_file vf ON vf.source_file_id = f.source_file_id
158                 JOIN package_version pv ON pv.id = vf.package_version_id
159                 GROUP BY e.level, e.hash
160                 HAVING COUNT(DISTINCT pv.package_id) >= 2;
161
162             DELETE FROM fingerprint_stats
163                 WHERE (level, hash) IN (SELECT level, hash FROM stats_pending_fingerprint);
164             INSERT INTO fingerprint_stats (level, hash, package_count, function_count)
165                 SELECT g.level, g.hash, COUNT(DISTINCT pv.package_id), COUNT(DISTINCT g.function_id)
166                 FROM stats_pending_fingerprint p
167                 JOIN fingerprint g ON g.level = p.level AND g.hash = p.hash
168                 JOIN function f ON f.id = g.function_id
169                 JOIN version_file vf ON vf.source_file_id = f.source_file_id
170                 JOIN package_version pv ON pv.id = vf.package_version_id
171                 GROUP BY g.level, g.hash
172                 HAVING COUNT(DISTINCT pv.package_id) >= 2;",
173        )?;
174        finish_refresh(&tx)?;
175        tx.commit()?;
176        self.hash_stats_info()
177    }
178
179    /// State of the hash statistics, without rebuilding them.
180    ///
181    /// # Errors
182    ///
183    /// [`Error`](crate::Error) when the database cannot be read.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use pelicode_core::Config;
189    /// use pelicode_db::Database;
190    ///
191    /// let dir = tempfile::tempdir().unwrap();
192    /// let db = Database::open(dir.path().join("pelicode.db"), &Config::default()).unwrap();
193    /// let info = db.hash_stats_info().unwrap();
194    /// assert_eq!(info.updated_at, None);
195    /// assert_eq!(info.packages, 0);
196    /// ```
197    pub fn hash_stats_info(&self) -> Result<HashStatsInfo> {
198        let meta = self.meta()?;
199        let count = |table: &str| -> Result<usize> {
200            let n: i64 =
201                self.conn
202                    .query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0))?;
203            Ok(n as usize)
204        };
205        let generation = meta.get("index_generation").map_or("0", String::as_str);
206        let updated_at = meta.get("stats_updated_at").cloned();
207        Ok(HashStatsInfo {
208            packages: self.package_count()?,
209            stale: meta.get("stats_generation").map(String::as_str) != Some(generation),
210            common_exact_hashes: count("exact_stats")?,
211            common_fingerprints: count("fingerprint_stats")?,
212            incremental: updated_at.is_some()
213                && meta.get(PENDING_COMPLETE).map(String::as_str) == Some("1"),
214            pending_hashes: count("stats_pending_exact")? + count("stats_pending_fingerprint")?,
215            updated_at,
216        })
217    }
218
219    /// Packages with at least one indexed file.
220    ///
221    /// # Errors
222    ///
223    /// [`Error`](crate::Error) when the database cannot be read.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// # use pelicode_core::{Config, Language, extract_bytes};
229    /// # use pelicode_db::{Database, PackageInfo};
230    /// # let dir = tempfile::tempdir().unwrap();
231    /// # let config = Config { min_tokens: 0, ..Config::default() };
232    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
233    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
234    /// # let file = extract_bytes("calc.py".as_ref(), Language::Python, source, &config).unwrap();
235    /// // The same function in two packages.
236    /// for name in ["calc", "copy"] {
237    ///     let package = PackageInfo {
238    ///         ecosystem: "pypi".into(),
239    ///         name: name.into(),
240    ///         version: "1.0".into(),
241    ///         ..PackageInfo::default()
242    ///     };
243    ///     db.index(&package, &[file.clone()]).unwrap();
244    /// }
245    /// assert_eq!(db.package_count().unwrap(), 2);
246    /// ```
247    pub fn package_count(&self) -> Result<usize> {
248        let n: i64 = self.conn.query_row(
249            "SELECT COUNT(DISTINCT pv.package_id)
250             FROM version_file vf JOIN package_version pv ON pv.id = vf.package_version_id",
251            [],
252            |r| r.get(0),
253        )?;
254        Ok(n as usize)
255    }
256
257    /// Packages containing a function with this exact hash (1 when not common).
258    ///
259    /// Counts as of the last [`Database::refresh_hash_stats`].
260    ///
261    /// # Errors
262    ///
263    /// [`Error`](crate::Error) when the database cannot be read.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// # use pelicode_core::{Config, Language, extract_bytes};
269    /// # use pelicode_db::{Database, PackageInfo};
270    /// # let dir = tempfile::tempdir().unwrap();
271    /// # let config = Config { min_tokens: 0, ..Config::default() };
272    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
273    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
274    /// # let file = extract_bytes("calc.py".as_ref(), Language::Python, source, &config).unwrap();
275    /// // The same function in two packages.
276    /// for name in ["calc", "copy"] {
277    ///     let package = PackageInfo {
278    ///         ecosystem: "pypi".into(),
279    ///         name: name.into(),
280    ///         version: "1.0".into(),
281    ///         ..PackageInfo::default()
282    ///     };
283    ///     db.index(&package, &[file.clone()]).unwrap();
284    /// }
285    /// use pelicode_core::Level;
286    ///
287    /// db.refresh_hash_stats().unwrap();
288    /// let hash = &file.functions[0].exact_hashes[&Level::N1];
289    /// assert_eq!(db.exact_package_count(Level::N1, hash).unwrap(), 2);
290    /// ```
291    pub fn exact_package_count(&self, level: Level, hash: &Hash128) -> Result<usize> {
292        let mut stmt = self.conn.prepare_cached(
293            "SELECT package_count FROM exact_stats WHERE level = ?1 AND hash = ?2",
294        )?;
295        let n: Option<i64> = stmt
296            .query_row(params![level.as_u8(), &hash.0[..]], |r| r.get(0))
297            .optional()?;
298        Ok(n.unwrap_or(1) as usize)
299    }
300
301    /// Packages containing this fingerprint (1 when not common).
302    ///
303    /// Counts as of the last [`Database::refresh_hash_stats`].
304    ///
305    /// # Errors
306    ///
307    /// [`Error`](crate::Error) when the database cannot be read.
308    ///
309    /// # Examples
310    ///
311    /// ```
312    /// # use pelicode_core::{Config, Language, extract_bytes};
313    /// # use pelicode_db::{Database, PackageInfo};
314    /// # let dir = tempfile::tempdir().unwrap();
315    /// # let config = Config { min_tokens: 0, ..Config::default() };
316    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
317    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
318    /// # let file = extract_bytes("calc.py".as_ref(), Language::Python, source, &config).unwrap();
319    /// // The same function in two packages.
320    /// for name in ["calc", "copy"] {
321    ///     let package = PackageInfo {
322    ///         ecosystem: "pypi".into(),
323    ///         name: name.into(),
324    ///         version: "1.0".into(),
325    ///         ..PackageInfo::default()
326    ///     };
327    ///     db.index(&package, &[file.clone()]).unwrap();
328    /// }
329    /// use pelicode_core::Level;
330    ///
331    /// db.refresh_hash_stats().unwrap();
332    /// let fingerprint = file.functions[0].features[&Level::N1].fingerprints[0].hash;
333    /// assert_eq!(db.fingerprint_package_count(Level::N1, fingerprint).unwrap(), 2);
334    /// assert_eq!(db.fingerprint_package_count(Level::N1, 42).unwrap(), 1);
335    /// ```
336    pub fn fingerprint_package_count(&self, level: Level, hash: u64) -> Result<usize> {
337        let mut stmt = self.conn.prepare_cached(
338            "SELECT package_count FROM fingerprint_stats WHERE level = ?1 AND hash = ?2",
339        )?;
340        let n: Option<i64> = stmt
341            .query_row(params![level.as_u8(), hash as i64], |r| r.get(0))
342            .optional()?;
343        Ok(n.unwrap_or(1) as usize)
344    }
345
346    /// The exact hashes at `level` of functions with at least `min_tokens`
347    /// tokens, shared by the most packages.
348    ///
349    /// Counts as of the last [`Database::refresh_hash_stats`].
350    ///
351    /// # Errors
352    ///
353    /// [`Error`](crate::Error) when the database cannot be read.
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// # use pelicode_core::{Config, Language, extract_bytes};
359    /// # use pelicode_db::{Database, PackageInfo};
360    /// # let dir = tempfile::tempdir().unwrap();
361    /// # let config = Config { min_tokens: 0, ..Config::default() };
362    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
363    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
364    /// # let file = extract_bytes("calc.py".as_ref(), Language::Python, source, &config).unwrap();
365    /// // The same function in two packages.
366    /// for name in ["calc", "copy"] {
367    ///     let package = PackageInfo {
368    ///         ecosystem: "pypi".into(),
369    ///         name: name.into(),
370    ///         version: "1.0".into(),
371    ///         ..PackageInfo::default()
372    ///     };
373    ///     db.index(&package, &[file.clone()]).unwrap();
374    /// }
375    /// use pelicode_core::Level;
376    ///
377    /// db.refresh_hash_stats().unwrap();
378    /// let common = db.common_exact_hashes(Level::N1, 0, 10).unwrap();
379    /// assert_eq!(common[0].package_count, 2);
380    /// assert!(common[0].sample.as_deref().unwrap().contains("total"));
381    /// ```
382    pub fn common_exact_hashes(
383        &self,
384        level: Level,
385        min_tokens: usize,
386        limit: usize,
387    ) -> Result<Vec<CommonHash>> {
388        let mut stmt = self.conn.prepare(
389            "SELECT s.hash, s.package_count, s.function_count FROM exact_stats s
390             WHERE s.level = ?1 AND EXISTS (
391                 SELECT 1 FROM exact_hash e JOIN function f ON f.id = e.function_id
392                 WHERE e.level = s.level AND e.hash = s.hash AND f.token_count >= ?3)
393             ORDER BY s.package_count DESC, s.function_count DESC LIMIT ?2",
394        )?;
395        let rows: Vec<(Vec<u8>, i64, i64)> = stmt
396            .query_map(
397                params![level.as_u8(), limit as i64, min_tokens as i64],
398                |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
399            )?
400            .collect::<rusqlite::Result<_>>()?;
401        rows.into_iter()
402            .map(|(hash, packages, functions)| {
403                let function: Option<i64> = self
404                    .conn
405                    .query_row(
406                        "SELECT e.function_id FROM exact_hash e JOIN function f ON f.id = e.function_id
407                         WHERE e.level = ?1 AND e.hash = ?2 AND f.token_count >= ?3 LIMIT 1",
408                        params![level.as_u8(), &hash, min_tokens as i64],
409                        |r| r.get(0),
410                    )
411                    .optional()?;
412                Ok(CommonHash {
413                    level,
414                    hash: pelicode_core::hash::to_hex(&hash),
415                    package_count: packages as usize,
416                    function_count: functions as usize,
417                    sample: match function {
418                        Some(id) => self.sample(id, None)?,
419                        None => None,
420                    },
421                })
422            })
423            .collect()
424    }
425
426    /// The fingerprints at `level` shared by the most packages.
427    ///
428    /// Counts as of the last [`Database::refresh_hash_stats`].
429    ///
430    /// # Errors
431    ///
432    /// [`Error`](crate::Error) when the database cannot be read.
433    ///
434    /// # Examples
435    ///
436    /// ```
437    /// # use pelicode_core::{Config, Language, extract_bytes};
438    /// # use pelicode_db::{Database, PackageInfo};
439    /// # let dir = tempfile::tempdir().unwrap();
440    /// # let config = Config { min_tokens: 0, ..Config::default() };
441    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
442    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
443    /// # let file = extract_bytes("calc.py".as_ref(), Language::Python, source, &config).unwrap();
444    /// // The same function in two packages.
445    /// for name in ["calc", "copy"] {
446    ///     let package = PackageInfo {
447    ///         ecosystem: "pypi".into(),
448    ///         name: name.into(),
449    ///         version: "1.0".into(),
450    ///         ..PackageInfo::default()
451    ///     };
452    ///     db.index(&package, &[file.clone()]).unwrap();
453    /// }
454    /// use pelicode_core::Level;
455    ///
456    /// db.refresh_hash_stats().unwrap();
457    /// let common = db.common_fingerprints(Level::N1, 3).unwrap();
458    /// assert!(!common.is_empty() && common.len() <= 3);
459    /// assert!(common.iter().all(|c| c.package_count == 2));
460    /// ```
461    pub fn common_fingerprints(&self, level: Level, limit: usize) -> Result<Vec<CommonHash>> {
462        let mut stmt = self.conn.prepare(
463            "SELECT hash, package_count, function_count FROM fingerprint_stats
464             WHERE level = ?1 ORDER BY package_count DESC, function_count DESC LIMIT ?2",
465        )?;
466        let rows: Vec<(i64, i64, i64)> = stmt
467            .query_map(params![level.as_u8(), limit as i64], |r| {
468                Ok((r.get(0)?, r.get(1)?, r.get(2)?))
469            })?
470            .collect::<rusqlite::Result<_>>()?;
471        rows.into_iter()
472            .map(|(hash, packages, functions)| {
473                let hit: Option<(i64, u32, u32)> = self
474                    .conn
475                    .query_row(
476                        "SELECT function_id, start_line, end_line FROM fingerprint
477                         WHERE level = ?1 AND hash = ?2 LIMIT 1",
478                        params![level.as_u8(), hash],
479                        |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
480                    )
481                    .optional()?;
482                Ok(CommonHash {
483                    level,
484                    hash: format!("{:016x}", hash as u64),
485                    package_count: packages as usize,
486                    function_count: functions as usize,
487                    sample: match hit {
488                        Some((id, start, end)) => self.sample(id, Some((start, end)))?,
489                        None => None,
490                    },
491                })
492            })
493            .collect()
494    }
495
496    fn sample(&self, function_id: i64, lines: Option<(u32, u32)>) -> Result<Option<String>> {
497        let info = self.function_info(function_id)?;
498        let Some(o) = self.occurrences(function_id)?.into_iter().next() else {
499            return Ok(None);
500        };
501        let (start, end) = lines.unwrap_or((info.start_line as u32, info.end_line as u32));
502        Ok(Some(format!(
503            "{}:{}@{} {}:{start}-{end} {}",
504            o.ecosystem, o.package, o.version, o.path, info.name
505        )))
506    }
507}
508
509/// Key of `meta` telling that the pending tables hold every hash whose
510/// package count may have changed since the last statistics.
511const PENDING_COMPLETE: &str = "stats_pending_complete";
512
513/// Records the end of a refresh: no hash is pending, the statistics match
514/// the current index generation, and the pending tables are complete again.
515fn finish_refresh(conn: &Connection) -> Result<()> {
516    conn.execute_batch(
517        "DELETE FROM stats_pending_exact;
518         DELETE FROM stats_pending_fingerprint;
519         INSERT INTO meta (key, value)
520             VALUES ('stats_generation',
521                     COALESCE((SELECT value FROM meta WHERE key = 'index_generation'), '0'))
522             ON CONFLICT (key) DO UPDATE SET value = excluded.value;
523         INSERT INTO meta (key, value)
524             VALUES ('stats_updated_at', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
525             ON CONFLICT (key) DO UPDATE SET value = excluded.value;",
526    )?;
527    set_pending_complete(conn, true)
528}
529
530/// Whether indexing must record pending hashes: only once statistics exist
531/// and every change since them is recorded.
532pub(crate) fn pending_complete(conn: &Connection) -> Result<bool> {
533    let value: Option<String> = conn
534        .query_row(
535            "SELECT value FROM meta WHERE key = ?1",
536            [PENDING_COMPLETE],
537            |r| r.get(0),
538        )
539        .optional()?;
540    Ok(value.as_deref() == Some("1"))
541}
542
543pub(crate) fn set_pending_complete(conn: &Connection, complete: bool) -> Result<()> {
544    conn.execute(
545        "INSERT INTO meta (key, value) VALUES (?1, ?2)
546         ON CONFLICT (key) DO UPDATE SET value = excluded.value",
547        params![PENDING_COMPLETE, if complete { "1" } else { "0" }],
548    )?;
549    Ok(())
550}
551
552/// Marks the hashes of the files currently linked to a package version as
553/// pending.
554pub(crate) fn mark_pending(conn: &Connection, version_id: i64) -> Result<()> {
555    conn.execute(
556        "INSERT OR IGNORE INTO stats_pending_exact (level, hash)
557             SELECT e.level, e.hash
558             FROM version_file vf
559             JOIN function f ON f.source_file_id = vf.source_file_id
560             JOIN exact_hash e ON e.function_id = f.id
561             WHERE vf.package_version_id = ?1",
562        [version_id],
563    )?;
564    conn.execute(
565        "INSERT OR IGNORE INTO stats_pending_fingerprint (level, hash)
566             SELECT g.level, g.hash
567             FROM version_file vf
568             JOIN function f ON f.source_file_id = vf.source_file_id
569             JOIN fingerprint g ON g.function_id = f.id
570             WHERE vf.package_version_id = ?1",
571        [version_id],
572    )?;
573    Ok(())
574}