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}