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}