Skip to main content

pelicode_db/
query.rs

1//! Lookups used by the detector.
2
3use pelicode_core::{Hash128, Level};
4use rusqlite::params;
5use serde::Serialize;
6
7use crate::{Database, Result};
8
9/// A stored function, without its occurrences.
10#[derive(Debug, Clone, Serialize)]
11pub struct FunctionInfo {
12    /// Identifier of the function in the database.
13    pub id: i64,
14    /// Language of the function.
15    pub language: String,
16    /// Name of the function (for display).
17    pub name: String,
18    /// Tree-sitter node kind of the function.
19    pub kind: String,
20    /// First line (1-based).
21    pub start_line: usize,
22    /// Last line (1-based, inclusive).
23    pub end_line: usize,
24    /// Start of the byte range in the file.
25    pub start_byte: usize,
26    /// End of the byte range in the file (exclusive).
27    pub end_byte: usize,
28    /// Number of tokens.
29    pub token_count: usize,
30}
31
32/// Where a stored function appears: a file of a package version.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34pub struct Occurrence {
35    /// Ecosystem of the package.
36    pub ecosystem: String,
37    /// Name of the package.
38    pub package: String,
39    /// Version of the package.
40    pub version: String,
41    /// Path of the file in the package.
42    pub path: String,
43    /// License of the package version.
44    pub license: Option<String>,
45    /// Publication date and time of the version (RFC 3339).
46    pub published_at: Option<String>,
47    /// URL of the source repository of the package.
48    pub repository_url: Option<String>,
49    /// URL the source archive was downloaded from.
50    pub archive_url: Option<String>,
51    /// SHA-256 of the file content (hex).
52    pub content_sha256: String,
53}
54
55/// A stored fingerprint with the source lines it covers.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct StoredFingerprint {
58    /// Hash of the k-gram.
59    pub hash: u64,
60    /// First source line covered (1-based).
61    pub start_line: u32,
62    /// Last source line covered (1-based, inclusive).
63    pub end_line: u32,
64}
65
66impl Database {
67    /// Functions whose exact hash at `level` is `hash`.
68    ///
69    /// # Errors
70    ///
71    /// [`Error`](crate::Error) when the database cannot be read.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// # use pelicode_core::{Config, Language, extract_bytes};
77    /// # use pelicode_db::{Database, PackageInfo};
78    /// # let dir = tempfile::tempdir().unwrap();
79    /// # let config = Config { min_tokens: 0, ..Config::default() };
80    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
81    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
82    /// # let file = extract_bytes("src/calc.py".as_ref(), Language::Python, source, &config).unwrap();
83    /// # let package = PackageInfo {
84    /// #     ecosystem: "pypi".into(), name: "calc".into(), version: "1.0".into(),
85    /// #     license_spdx: Some("MIT".into()), ..PackageInfo::default()
86    /// # };
87    /// # db.index(&package, &[file.clone()]).unwrap();
88    /// # let function = &file.functions[0];
89    /// use pelicode_core::Level;
90    ///
91    /// let ids = db.functions_by_exact_hash(Level::N1, &function.exact_hashes[&Level::N1]).unwrap();
92    /// assert_eq!(ids.len(), 1);
93    /// ```
94    pub fn functions_by_exact_hash(&self, level: Level, hash: &Hash128) -> Result<Vec<i64>> {
95        let mut stmt = self
96            .conn
97            .prepare_cached("SELECT function_id FROM exact_hash WHERE level = ?1 AND hash = ?2")?;
98        let ids = stmt.query_map(params![level.as_u8(), &hash.0[..]], |r| r.get(0))?;
99        Ok(ids.collect::<rusqlite::Result<_>>()?)
100    }
101
102    /// Functions sharing LSH bucket `bucket` of `band` at `level`.
103    ///
104    /// # Errors
105    ///
106    /// [`Error`](crate::Error) when the database cannot be read.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// # use pelicode_core::{Config, Language, extract_bytes};
112    /// # use pelicode_db::{Database, PackageInfo};
113    /// # let dir = tempfile::tempdir().unwrap();
114    /// # let config = Config { min_tokens: 0, ..Config::default() };
115    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
116    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
117    /// # let file = extract_bytes("src/calc.py".as_ref(), Language::Python, source, &config).unwrap();
118    /// # let package = PackageInfo {
119    /// #     ecosystem: "pypi".into(), name: "calc".into(), version: "1.0".into(),
120    /// #     license_spdx: Some("MIT".into()), ..PackageInfo::default()
121    /// # };
122    /// # db.index(&package, &[file.clone()]).unwrap();
123    /// # let function = &file.functions[0];
124    /// use pelicode_core::Level;
125    ///
126    /// let buckets = &function.features[&Level::N1].lsh_buckets;
127    /// assert_eq!(db.functions_by_lsh_bucket(Level::N1, 0, buckets[0]).unwrap().len(), 1);
128    /// ```
129    pub fn functions_by_lsh_bucket(
130        &self,
131        level: Level,
132        band: usize,
133        bucket: u64,
134    ) -> Result<Vec<i64>> {
135        let mut stmt = self.conn.prepare_cached(
136            "SELECT function_id FROM lsh_bucket WHERE level = ?1 AND band = ?2 AND bucket_hash = ?3",
137        )?;
138        let ids = stmt.query_map(params![level.as_u8(), band as i64, bucket as i64], |r| {
139            r.get(0)
140        })?;
141        Ok(ids.collect::<rusqlite::Result<_>>()?)
142    }
143
144    /// Fingerprints of a stored function at `level`.
145    ///
146    /// # Errors
147    ///
148    /// [`Error`](crate::Error) when the database cannot be read.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// # use pelicode_core::{Config, Language, extract_bytes};
154    /// # use pelicode_db::{Database, PackageInfo};
155    /// # let dir = tempfile::tempdir().unwrap();
156    /// # let config = Config { min_tokens: 0, ..Config::default() };
157    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
158    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
159    /// # let file = extract_bytes("src/calc.py".as_ref(), Language::Python, source, &config).unwrap();
160    /// # let package = PackageInfo {
161    /// #     ecosystem: "pypi".into(), name: "calc".into(), version: "1.0".into(),
162    /// #     license_spdx: Some("MIT".into()), ..PackageInfo::default()
163    /// # };
164    /// # db.index(&package, &[file.clone()]).unwrap();
165    /// # let function = &file.functions[0];
166    /// use pelicode_core::Level;
167    ///
168    /// let id = db.functions_by_exact_hash(Level::N1, &function.exact_hashes[&Level::N1]).unwrap()[0];
169    /// let stored = db.fingerprints(id, Level::N1).unwrap();
170    /// assert_eq!(stored.len(), function.features[&Level::N1].fingerprints.len());
171    /// assert!(stored.iter().all(|f| (1..=5).contains(&f.start_line)));
172    /// ```
173    pub fn fingerprints(&self, function_id: i64, level: Level) -> Result<Vec<StoredFingerprint>> {
174        let mut stmt = self.conn.prepare_cached(
175            "SELECT hash, start_line, end_line FROM fingerprint
176             WHERE function_id = ?1 AND level = ?2 ORDER BY position",
177        )?;
178        let rows = stmt.query_map(params![function_id, level.as_u8()], |r| {
179            Ok(StoredFingerprint {
180                hash: r.get::<_, i64>(0)? as u64,
181                start_line: r.get(1)?,
182                end_line: r.get(2)?,
183            })
184        })?;
185        Ok(rows.collect::<rusqlite::Result<_>>()?)
186    }
187
188    /// A stored function, without its occurrences.
189    ///
190    /// # Errors
191    ///
192    /// [`Error`](crate::Error) when no function has the identifier or the database
193    /// cannot be read.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// # use pelicode_core::{Config, Language, extract_bytes};
199    /// # use pelicode_db::{Database, PackageInfo};
200    /// # let dir = tempfile::tempdir().unwrap();
201    /// # let config = Config { min_tokens: 0, ..Config::default() };
202    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
203    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
204    /// # let file = extract_bytes("src/calc.py".as_ref(), Language::Python, source, &config).unwrap();
205    /// # let package = PackageInfo {
206    /// #     ecosystem: "pypi".into(), name: "calc".into(), version: "1.0".into(),
207    /// #     license_spdx: Some("MIT".into()), ..PackageInfo::default()
208    /// # };
209    /// # db.index(&package, &[file.clone()]).unwrap();
210    /// # let function = &file.functions[0];
211    /// use pelicode_core::Level;
212    ///
213    /// let id = db.functions_by_exact_hash(Level::N1, &function.exact_hashes[&Level::N1]).unwrap()[0];
214    /// let info = db.function_info(id).unwrap();
215    /// assert_eq!((info.name.as_str(), info.start_line, info.end_line), ("total", 1, 5));
216    /// ```
217    pub fn function_info(&self, function_id: i64) -> Result<FunctionInfo> {
218        let mut stmt = self.conn.prepare_cached(
219            "SELECT f.id, sf.language, f.name, f.kind, f.start_line, f.end_line,
220                    f.start_byte, f.end_byte, f.token_count
221             FROM function f JOIN source_file sf ON sf.id = f.source_file_id
222             WHERE f.id = ?1",
223        )?;
224        Ok(stmt.query_row([function_id], |r| {
225            Ok(FunctionInfo {
226                id: r.get(0)?,
227                language: r.get(1)?,
228                name: r.get(2)?,
229                kind: r.get(3)?,
230                start_line: r.get::<_, i64>(4)? as usize,
231                end_line: r.get::<_, i64>(5)? as usize,
232                start_byte: r.get::<_, i64>(6)? as usize,
233                end_byte: r.get::<_, i64>(7)? as usize,
234                token_count: r.get::<_, i64>(8)? as usize,
235            })
236        })?)
237    }
238
239    /// Every place a stored function appears, earliest publication first.
240    ///
241    /// # Errors
242    ///
243    /// [`Error`](crate::Error) when the database cannot be read.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// # use pelicode_core::{Config, Language, extract_bytes};
249    /// # use pelicode_db::{Database, PackageInfo};
250    /// # let dir = tempfile::tempdir().unwrap();
251    /// # let config = Config { min_tokens: 0, ..Config::default() };
252    /// # let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
253    /// # let source = b"def total(xs):\n    s = 0\n    for x in xs:\n        s += x * 2\n    return s\n";
254    /// # let file = extract_bytes("src/calc.py".as_ref(), Language::Python, source, &config).unwrap();
255    /// # let package = PackageInfo {
256    /// #     ecosystem: "pypi".into(), name: "calc".into(), version: "1.0".into(),
257    /// #     license_spdx: Some("MIT".into()), ..PackageInfo::default()
258    /// # };
259    /// # db.index(&package, &[file.clone()]).unwrap();
260    /// # let function = &file.functions[0];
261    /// use pelicode_core::Level;
262    ///
263    /// let id = db.functions_by_exact_hash(Level::N1, &function.exact_hashes[&Level::N1]).unwrap()[0];
264    /// let occurrences = db.occurrences(id).unwrap();
265    /// assert_eq!(occurrences[0].package, "calc");
266    /// assert_eq!(occurrences[0].path, "src/calc.py");
267    /// ```
268    pub fn occurrences(&self, function_id: i64) -> Result<Vec<Occurrence>> {
269        let mut stmt = self.conn.prepare_cached(
270            "SELECT e.name, p.name, pv.version, vf.path, pv.license_spdx, pv.published_at,
271                    p.repository_url, pv.archive_url, sf.content_sha256
272             FROM function f
273             JOIN source_file sf ON sf.id = f.source_file_id
274             JOIN version_file vf ON vf.source_file_id = sf.id
275             JOIN package_version pv ON pv.id = vf.package_version_id
276             JOIN package p ON p.id = pv.package_id
277             JOIN ecosystem e ON e.id = p.ecosystem_id
278             WHERE f.id = ?1
279             ORDER BY pv.published_at IS NULL, pv.published_at, e.name, p.name, pv.version, vf.path",
280        )?;
281        let rows = stmt.query_map([function_id], |r| {
282            Ok(Occurrence {
283                ecosystem: r.get(0)?,
284                package: r.get(1)?,
285                version: r.get(2)?,
286                path: r.get(3)?,
287                license: r.get(4)?,
288                published_at: r.get(5)?,
289                repository_url: r.get(6)?,
290                archive_url: r.get(7)?,
291                content_sha256: r.get(8)?,
292            })
293        })?;
294        Ok(rows.collect::<rusqlite::Result<_>>()?)
295    }
296}