1use std::path::PathBuf;
17
18#[derive(Debug, Clone)]
24pub struct ResolvedPackage {
25 pub path: PathBuf,
27 pub name: String,
29 pub is_namespace: bool,
31}
32
33pub fn get_global_cache_dir() -> Option<PathBuf> {
39 let cache_base = if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
40 PathBuf::from(xdg)
41 } else if let Ok(home) = std::env::var("HOME") {
42 PathBuf::from(home).join(".cache")
43 } else if let Ok(home) = std::env::var("USERPROFILE") {
44 PathBuf::from(home).join(".cache")
45 } else {
46 return None;
47 };
48
49 let normalize_cache = cache_base.join("normalize");
50 if !normalize_cache.exists() {
51 std::fs::create_dir_all(&normalize_cache).ok()?;
52 }
53
54 Some(normalize_cache)
55}
56
57pub fn get_global_packages_db() -> Option<PathBuf> {
59 let cache = get_global_cache_dir()?;
60 Some(cache.join("packages.db"))
61}
62
63use libsql::{Connection, Database, params};
68
69#[derive(Debug, thiserror::Error)]
71pub enum PackageIndexError {
72 #[error("failed to open package index: {0}")]
73 Open(#[from] libsql::Error),
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct Version {
79 pub major: u32,
80 pub minor: u32,
81}
82
83impl Version {
84 pub fn parse(s: &str) -> Option<Version> {
90 let parts: Vec<&str> = s.split('.').collect();
91 if parts.len() >= 2 {
92 Some(Version {
93 major: parts[0].parse().ok()?,
94 minor: parts[1].parse().ok()?,
95 })
96 } else {
97 None
98 }
99 }
100
101 pub fn in_range(&self, min: Version, max: Option<Version>) -> bool {
102 if *self < min {
103 return false;
104 }
105 if let Some(max) = max
106 && *self > max
107 {
108 return false;
109 }
110 true
111 }
112}
113
114impl PartialOrd for Version {
115 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
116 Some(self.cmp(other))
117 }
118}
119
120impl Ord for Version {
121 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
122 match self.major.cmp(&other.major) {
123 std::cmp::Ordering::Equal => self.minor.cmp(&other.minor),
124 ord => ord,
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct PackageRecord {
132 pub id: i64,
133 pub language: String,
134 pub name: String,
135 pub path: String,
136 pub min_major: u32,
137 pub min_minor: u32,
138 pub max_major: Option<u32>,
139 pub max_minor: Option<u32>,
140}
141
142impl PackageRecord {
143 pub fn min_version(&self) -> Version {
144 Version {
145 major: self.min_major,
146 minor: self.min_minor,
147 }
148 }
149
150 pub fn max_version(&self) -> Option<Version> {
151 match (self.max_major, self.max_minor) {
152 (Some(major), Some(minor)) => Some(Version { major, minor }),
153 _ => None,
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct SymbolRecord {
161 pub id: i64,
162 pub package_id: i64,
163 pub name: String,
164 pub kind: String,
165 pub signature: String,
166 pub line: u32,
167}
168
169pub struct PackageIndex {
171 conn: Connection,
172 #[allow(dead_code)]
173 db: Database,
174}
175
176impl PackageIndex {
177 pub async fn open() -> Result<Self, PackageIndexError> {
178 let db_path = get_global_packages_db().ok_or_else(|| {
179 libsql::Error::SqliteFailure(1, "Cannot determine cache directory".into())
180 })?;
181
182 let db = libsql::Builder::new_local(db_path).build().await?;
183 let conn = db.connect()?;
184 let index = PackageIndex { conn, db };
185 index.init_schema().await?;
186 Ok(index)
187 }
188
189 pub async fn open_in_memory() -> Result<Self, PackageIndexError> {
190 let db = libsql::Builder::new_local(":memory:").build().await?;
191 let conn = db.connect()?;
192 let index = PackageIndex { conn, db };
193 index.init_schema().await?;
194 Ok(index)
195 }
196
197 async fn init_schema(&self) -> Result<(), libsql::Error> {
198 self.conn
199 .execute(
200 "CREATE TABLE IF NOT EXISTS packages (
201 id INTEGER PRIMARY KEY,
202 language TEXT NOT NULL,
203 name TEXT NOT NULL,
204 path TEXT NOT NULL,
205 min_major INTEGER NOT NULL,
206 min_minor INTEGER NOT NULL,
207 max_major INTEGER,
208 max_minor INTEGER,
209 indexed_at INTEGER NOT NULL
210 )",
211 (),
212 )
213 .await?;
214
215 self.conn
216 .execute(
217 "CREATE INDEX IF NOT EXISTS idx_packages_lang_name ON packages(language, name)",
218 (),
219 )
220 .await?;
221
222 self.conn
223 .execute(
224 "CREATE TABLE IF NOT EXISTS symbols (
225 id INTEGER PRIMARY KEY,
226 package_id INTEGER NOT NULL,
227 name TEXT NOT NULL,
228 kind TEXT NOT NULL,
229 signature TEXT NOT NULL,
230 line INTEGER NOT NULL,
231 FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE
232 )",
233 (),
234 )
235 .await?;
236
237 self.conn
238 .execute(
239 "CREATE INDEX IF NOT EXISTS idx_symbols_package ON symbols(package_id)",
240 (),
241 )
242 .await?;
243 self.conn
244 .execute(
245 "CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)",
246 (),
247 )
248 .await?;
249
250 Ok(())
251 }
252
253 pub async fn insert_package(
254 &self,
255 language: &str,
256 name: &str,
257 path: &str,
258 min_version: Version,
259 max_version: Option<Version>,
260 ) -> Result<i64, libsql::Error> {
261 let now = std::time::SystemTime::now()
262 .duration_since(std::time::UNIX_EPOCH)
263 .unwrap_or_default()
264 .as_secs() as i64;
265
266 self.conn.execute(
267 "INSERT INTO packages (language, name, path, min_major, min_minor, max_major, max_minor, indexed_at)
268 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
269 params![
270 language,
271 name,
272 path,
273 min_version.major,
274 min_version.minor,
275 max_version.map(|v| v.major),
276 max_version.map(|v| v.minor),
277 now,
278 ],
279 ).await?;
280 Ok(self.conn.last_insert_rowid())
281 }
282
283 pub async fn insert_symbol(
284 &self,
285 package_id: i64,
286 name: &str,
287 kind: &str,
288 signature: &str,
289 line: u32,
290 ) -> Result<i64, libsql::Error> {
291 self.conn
292 .execute(
293 "INSERT INTO symbols (package_id, name, kind, signature, line)
294 VALUES (?1, ?2, ?3, ?4, ?5)",
295 params![package_id, name, kind, signature, line],
296 )
297 .await?;
298 Ok(self.conn.last_insert_rowid())
299 }
300
301 pub async fn find_package(
302 &self,
303 language: &str,
304 name: &str,
305 version: Option<Version>,
306 ) -> Result<Option<PackageRecord>, libsql::Error> {
307 let mut rows = self
308 .conn
309 .query(
310 "SELECT id, language, name, path, min_major, min_minor, max_major, max_minor
311 FROM packages WHERE language = ?1 AND name = ?2",
312 params![language, name],
313 )
314 .await?;
315
316 let mut packages = Vec::new();
317 while let Some(row) = rows.next().await? {
318 packages.push(PackageRecord {
319 id: row.get(0)?,
320 language: row.get(1)?,
321 name: row.get(2)?,
322 path: row.get(3)?,
323 min_major: row.get(4)?,
324 min_minor: row.get(5)?,
325 max_major: row.get(6)?,
326 max_minor: row.get(7)?,
327 });
328 }
329
330 if let Some(ver) = version {
331 for pkg in packages {
332 if ver.in_range(pkg.min_version(), pkg.max_version()) {
333 return Ok(Some(pkg));
334 }
335 }
336 Ok(None)
337 } else {
338 Ok(packages.into_iter().next())
339 }
340 }
341
342 pub async fn get_symbols(&self, package_id: i64) -> Result<Vec<SymbolRecord>, libsql::Error> {
343 let mut rows = self
344 .conn
345 .query(
346 "SELECT id, package_id, name, kind, signature, line
347 FROM symbols WHERE package_id = ?1 ORDER BY line",
348 params![package_id],
349 )
350 .await?;
351
352 let mut symbols = Vec::new();
353 while let Some(row) = rows.next().await? {
354 symbols.push(SymbolRecord {
355 id: row.get(0)?,
356 package_id: row.get(1)?,
357 name: row.get(2)?,
358 kind: row.get(3)?,
359 signature: row.get(4)?,
360 line: row.get(5)?,
361 });
362 }
363
364 Ok(symbols)
365 }
366
367 pub async fn find_symbol(
368 &self,
369 language: &str,
370 symbol_name: &str,
371 version: Option<Version>,
372 ) -> Result<Vec<(PackageRecord, SymbolRecord)>, libsql::Error> {
373 let mut rows = self.conn.query(
374 "SELECT p.id, p.language, p.name, p.path, p.min_major, p.min_minor, p.max_major, p.max_minor,
375 s.id, s.package_id, s.name, s.kind, s.signature, s.line
376 FROM symbols s
377 JOIN packages p ON s.package_id = p.id
378 WHERE p.language = ?1 AND s.name = ?2",
379 params![language, symbol_name],
380 ).await?;
381
382 let mut results = Vec::new();
383 while let Some(row) = rows.next().await? {
384 results.push((
385 PackageRecord {
386 id: row.get(0)?,
387 language: row.get(1)?,
388 name: row.get(2)?,
389 path: row.get(3)?,
390 min_major: row.get(4)?,
391 min_minor: row.get(5)?,
392 max_major: row.get(6)?,
393 max_minor: row.get(7)?,
394 },
395 SymbolRecord {
396 id: row.get(8)?,
397 package_id: row.get(9)?,
398 name: row.get(10)?,
399 kind: row.get(11)?,
400 signature: row.get(12)?,
401 line: row.get(13)?,
402 },
403 ));
404 }
405
406 if let Some(ver) = version {
407 Ok(results
408 .into_iter()
409 .filter(|(pkg, _)| ver.in_range(pkg.min_version(), pkg.max_version()))
410 .collect())
411 } else {
412 Ok(results)
413 }
414 }
415
416 pub async fn is_indexed(&self, language: &str, name: &str) -> Result<bool, libsql::Error> {
417 let mut rows = self
418 .conn
419 .query(
420 "SELECT COUNT(*) FROM packages WHERE language = ?1 AND name = ?2",
421 params![language, name],
422 )
423 .await?;
424
425 if let Some(row) = rows.next().await? {
426 let count: i64 = row.get(0)?;
427 Ok(count > 0)
428 } else {
429 Ok(false)
430 }
431 }
432
433 pub async fn delete_package(&self, package_id: i64) -> Result<(), libsql::Error> {
434 self.conn
435 .execute(
436 "DELETE FROM symbols WHERE package_id = ?1",
437 params![package_id],
438 )
439 .await?;
440 self.conn
441 .execute("DELETE FROM packages WHERE id = ?1", params![package_id])
442 .await?;
443 Ok(())
444 }
445
446 pub async fn clear(&self) -> Result<(), libsql::Error> {
447 self.conn.execute("DELETE FROM symbols", ()).await?;
448 self.conn.execute("DELETE FROM packages", ()).await?;
449 Ok(())
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_version_parsing() {
459 assert_eq!(
460 Version::parse("3.11"),
461 Some(Version {
462 major: 3,
463 minor: 11
464 })
465 );
466 assert_eq!(
467 Version::parse("1.21"),
468 Some(Version {
469 major: 1,
470 minor: 21
471 })
472 );
473 assert_eq!(Version::parse("invalid"), None);
474 }
475
476 #[test]
477 fn test_version_comparison() {
478 let v1 = Version {
479 major: 3,
480 minor: 10,
481 };
482 let v2 = Version {
483 major: 3,
484 minor: 11,
485 };
486 let v3 = Version { major: 4, minor: 0 };
487
488 assert!(v1 < v2);
489 assert!(v2 < v3);
490 assert!(v1.in_range(v1, Some(v2)));
491 assert!(!v3.in_range(v1, Some(v2)));
492 }
493
494 #[tokio::test]
495 async fn test_package_index() {
496 let index = PackageIndex::open_in_memory().await.unwrap();
497
498 let pkg_id = index
500 .insert_package(
501 "python",
502 "requests",
503 "/path/to/requests",
504 Version { major: 3, minor: 8 },
505 None,
506 )
507 .await
508 .unwrap();
509
510 index
512 .insert_symbol(pkg_id, "get", "function", "def get(url)", 10)
513 .await
514 .unwrap();
515
516 let found = index
518 .find_package("python", "requests", None)
519 .await
520 .unwrap();
521 assert!(found.is_some());
522 assert_eq!(found.unwrap().name, "requests");
523
524 let symbols = index.get_symbols(pkg_id).await.unwrap();
526 assert_eq!(symbols.len(), 1);
527 assert_eq!(symbols[0].name, "get");
528
529 assert!(index.is_indexed("python", "requests").await.unwrap());
531 assert!(!index.is_indexed("python", "nonexistent").await.unwrap());
532 }
533}