1use std::path::PathBuf;
8
9use anyhow::Result;
10use rusqlite::{Connection, params};
11use zoi_core::types;
12use zoi_resolver::resolve::{get_db_root, get_host_db_root};
13
14pub fn get_db_path(registry_handle: &str) -> Result<PathBuf> {
20 let target_root = get_db_root()?;
21 let target_path = target_root.join(format!("{registry_handle}.db"));
22
23 if !target_path.exists() && zoi_core::sysroot::get_sysroot().is_some() {
24 let host_root = get_host_db_root()?;
26 let host_path = host_root.join(format!("{registry_handle}.db"));
27
28 if host_path.exists() {
29 if let Some(parent) = target_path.parent() {
30 std::fs::create_dir_all(parent)?;
31 }
32 let _ = std::fs::copy(&host_path, &target_path);
34 }
35 }
36
37 Ok(target_path)
38}
39
40pub fn open_connection(registry_handle: &str) -> Result<Connection> {
48 let conn = open_connection_no_setup(registry_handle)?;
49 setup_schema(&conn)?;
50 Ok(conn)
51}
52
53pub fn open_connection_no_setup(registry_handle: &str) -> Result<Connection> {
65 let db_path = get_db_path(registry_handle)?;
66 if let Some(parent) = db_path.parent() {
67 std::fs::create_dir_all(parent)?;
68 }
69 let conn = Connection::open(db_path)?;
70 conn.busy_timeout(std::time::Duration::from_secs(5))?;
71
72 conn.execute_batch(
73 "
74 PRAGMA foreign_keys = ON;
75 PRAGMA journal_mode = WAL;
76 PRAGMA synchronous = NORMAL;
77 "
78 )?;
79
80 Ok(conn)
81}
82
83fn setup_schema(conn: &Connection) -> Result<()> {
94 conn.execute(
95 "CREATE TABLE IF NOT EXISTS packages (
96 id INTEGER PRIMARY KEY,
97 name TEXT NOT NULL,
98 sub_package TEXT,
99 repo TEXT NOT NULL,
100 version TEXT,
101 epoch INTEGER DEFAULT 0,
102 description TEXT,
103 package_type TEXT,
104 tags TEXT,
105 bins TEXT,
106 license TEXT,
107 registry TEXT,
108 scope TEXT,
109 reason TEXT,
110 dependencies TEXT,
111 revision TEXT,
112 archive_size INTEGER,
113 installed_size INTEGER,
114 UNIQUE(name, sub_package, repo, scope, registry)
115 )",
116 []
117 )?;
118
119 let has_epoch: bool = conn
120 .query_row(
121 "SELECT count(*) FROM pragma_table_info('packages') WHERE \
122 name='epoch'",
123 [],
124 |row| row.get(0)
125 )
126 .unwrap_or(0)
127 > 0;
128
129 if !has_epoch {
130 let _ = conn.execute(
131 "ALTER TABLE packages ADD COLUMN epoch INTEGER DEFAULT 0",
132 []
133 );
134 }
135
136 let has_revision: bool = conn
137 .query_row(
138 "SELECT count(*) FROM pragma_table_info('packages') WHERE \
139 name='revision'",
140 [],
141 |row| row.get(0)
142 )
143 .unwrap_or(0)
144 > 0;
145
146 if !has_revision {
147 let _ =
148 conn.execute("ALTER TABLE packages ADD COLUMN revision TEXT", []);
149 }
150
151 let has_deps: bool = conn
152 .query_row(
153 "SELECT count(*) FROM pragma_table_info('packages') WHERE \
154 name='dependencies'",
155 [],
156 |row| row.get(0)
157 )
158 .unwrap_or(0)
159 > 0;
160
161 if !has_deps {
162 let _ = conn
163 .execute("ALTER TABLE packages ADD COLUMN dependencies TEXT", []);
164 }
165
166 let column_exists: bool = conn
167 .query_row(
168 "SELECT count(*) FROM pragma_table_info('packages') WHERE \
169 name='bins'",
170 [],
171 |row| row.get(0)
172 )
173 .unwrap_or(0)
174 > 0;
175
176 if !column_exists {
177 let _ = conn.execute("ALTER TABLE packages ADD COLUMN bins TEXT", []);
178 }
179
180 let has_archive_size: bool = conn
181 .query_row(
182 "SELECT count(*) FROM pragma_table_info('packages') WHERE \
183 name='archive_size'",
184 [],
185 |row| row.get(0)
186 )
187 .unwrap_or(0)
188 > 0;
189
190 if !has_archive_size {
191 let _ = conn.execute(
192 "ALTER TABLE packages ADD COLUMN archive_size INTEGER",
193 []
194 );
195 let _ = conn.execute(
196 "ALTER TABLE packages ADD COLUMN installed_size INTEGER",
197 []
198 );
199 }
200
201 let has_archive_hash: bool = conn
202 .query_row(
203 "SELECT count(*) FROM pragma_table_info('packages') WHERE \
204 name='archive_hash'",
205 [],
206 |row| row.get(0)
207 )
208 .unwrap_or(0)
209 > 0;
210
211 if !has_archive_hash {
212 let _ = conn
213 .execute("ALTER TABLE packages ADD COLUMN archive_hash TEXT", []);
214 }
215
216 conn.execute(
217 "CREATE INDEX IF NOT EXISTS idx_packages_name ON packages(name)",
218 []
219 )?;
220 conn.execute(
221 "CREATE INDEX IF NOT EXISTS idx_packages_repo ON packages(repo)",
222 []
223 )?;
224
225 conn.execute(
226 "CREATE TABLE IF NOT EXISTS package_files (
227 id INTEGER PRIMARY KEY,
228 package_id INTEGER,
229 path TEXT NOT NULL,
230 FOREIGN KEY(package_id) REFERENCES packages(id) ON DELETE CASCADE
231 )",
232 []
233 )?;
234
235 conn.execute(
236 "CREATE TABLE IF NOT EXISTS package_advisories (
237 id TEXT PRIMARY KEY,
238 package TEXT NOT NULL,
239 sub_package TEXT,
240 summary TEXT NOT NULL,
241 severity TEXT NOT NULL,
242 cvss TEXT,
243 affected_range TEXT NOT NULL,
244 fixed_in TEXT,
245 description TEXT NOT NULL,
246 references_json TEXT,
247 repo TEXT,
248 registry TEXT
249 )",
250 []
251 )?;
252
253 let has_sub_pkg_adv: bool = conn
254 .query_row(
255 "SELECT count(*) FROM pragma_table_info('package_advisories') \
256 WHERE name='sub_package'",
257 [],
258 |row| row.get(0)
259 )
260 .unwrap_or(0)
261 > 0;
262
263 if !has_sub_pkg_adv {
264 let _ = conn.execute(
265 "ALTER TABLE package_advisories ADD COLUMN sub_package TEXT",
266 []
267 );
268 }
269
270 conn.execute(
271 "CREATE INDEX IF NOT EXISTS idx_package_advisories_package ON \
272 package_advisories(package, sub_package)",
273 []
274 )?;
275
276 conn.execute(
277 "CREATE INDEX IF NOT EXISTS idx_package_files_path ON \
278 package_files(path)",
279 []
280 )?;
281
282 let mut fts_needs_rebuild = false;
283 let fts_exists: bool = conn
284 .query_row(
285 "SELECT count(*) FROM sqlite_master WHERE type='table' AND \
286 name='packages_fts'",
287 [],
288 |row| row.get(0)
289 )
290 .unwrap_or(0)
291 > 0;
292
293 if fts_exists {
294 let has_bins_fts: bool = conn
295 .query_row(
296 "SELECT count(*) FROM pragma_table_info('packages_fts') WHERE \
297 name='bins'",
298 [],
299 |row| row.get(0)
300 )
301 .unwrap_or(0)
302 > 0;
303 if !has_bins_fts {
304 fts_needs_rebuild = true;
305 }
306 } else {
307 fts_needs_rebuild = true;
308 }
309
310 if fts_needs_rebuild {
311 let _ = conn.execute("DROP TABLE IF EXISTS packages_fts", []);
312 let _ = conn.execute("DROP TRIGGER IF EXISTS packages_ai", []);
313 let _ = conn.execute("DROP TRIGGER IF EXISTS packages_ad", []);
314 let _ = conn.execute("DROP TRIGGER IF EXISTS packages_au", []);
315
316 let _ = conn.execute(
317 "CREATE VIRTUAL TABLE packages_fts USING fts5(name, description, \
318 tags, bins, content='packages', content_rowid='id')",
319 []
320 );
321
322 let _ = conn.execute(
323 "INSERT INTO packages_fts(rowid, name, description, tags, bins)
324 SELECT id, name, description, tags, bins FROM packages",
325 []
326 );
327
328 let _ = conn.execute(
329 "CREATE TRIGGER packages_ai AFTER INSERT ON packages BEGIN
330 INSERT INTO packages_fts(rowid, name, description, tags, bins) \
331 VALUES (new.id, new.name, new.description, new.tags, new.bins);
332 END",
333 []
334 );
335 let _ = conn.execute(
336 "CREATE TRIGGER packages_ad AFTER DELETE ON packages BEGIN
337 INSERT INTO packages_fts(packages_fts, rowid, name, \
338 description, tags, bins) VALUES('delete', old.id, old.name, \
339 old.description, old.tags, old.bins);
340 END",
341 []
342 );
343 let _ = conn.execute(
344 "CREATE TRIGGER packages_au AFTER UPDATE ON packages BEGIN
345 INSERT INTO packages_fts(packages_fts, rowid, name, \
346 description, tags, bins) VALUES('delete', old.id, old.name, \
347 old.description, old.tags, old.bins);
348 INSERT INTO packages_fts(rowid, name, description, tags, bins) \
349 VALUES (new.id, new.name, new.description, new.tags, new.bins);
350 END",
351 []
352 );
353 }
354
355 let files_fts_exists: bool = conn
356 .query_row(
357 "SELECT count(*) FROM sqlite_master WHERE type='table' AND \
358 name='package_files_fts'",
359 [],
360 |row| row.get(0)
361 )
362 .unwrap_or(0)
363 > 0;
364
365 if !files_fts_exists {
366 let _ = conn.execute(
367 "CREATE VIRTUAL TABLE package_files_fts USING fts5(path, \
368 content='package_files', content_rowid='id')",
369 []
370 );
371
372 let _ = conn.execute(
373 "INSERT INTO package_files_fts(rowid, path) SELECT id, path FROM \
374 package_files",
375 []
376 );
377
378 let _ = conn.execute(
379 "CREATE TRIGGER package_files_ai AFTER INSERT ON package_files \
380 BEGIN
381 INSERT INTO package_files_fts(rowid, path) VALUES (new.id, \
382 new.path);
383 END",
384 []
385 );
386 let _ = conn.execute(
387 "CREATE TRIGGER package_files_ad AFTER DELETE ON package_files \
388 BEGIN
389 INSERT INTO package_files_fts(package_files_fts, rowid, path) \
390 VALUES('delete', old.id, old.path);
391 END",
392 []
393 );
394 let _ = conn.execute(
395 "CREATE TRIGGER package_files_au AFTER UPDATE ON package_files \
396 BEGIN
397 INSERT INTO package_files_fts(package_files_fts, rowid, path) \
398 VALUES('delete', old.id, old.path);
399 INSERT INTO package_files_fts(rowid, path) VALUES (new.id, \
400 new.path);
401 END",
402 []
403 );
404 }
405
406 Ok(())
407}
408
409pub fn update_package(
415 conn: &Connection,
416 pkg: &types::Package,
417 registry: &str,
418 scope: Option<types::Scope>,
419 sub_package: Option<&str>,
420 reason: Option<&types::InstallReason>
421) -> Result<i64> {
422 let tags_json = serde_json::to_string(&pkg.tags)?;
423 let bins_json =
424 serde_json::to_string(&pkg.bins.as_ref().unwrap_or(&vec![]))
425 .unwrap_or_default();
426 let pkg_type = format!("{:?}", pkg.package_type).to_lowercase();
427 let scope_str = scope.map(|s| format!("{s:?}").to_lowercase());
428 let reason_str = reason.map(|r| match r {
429 types::InstallReason::Direct => "direct".to_string(),
430 types::InstallReason::Dependency { parent } => {
431 format!("dependency:{parent}")
432 }
433 });
434
435 let deps_json = if let Some(deps) = &pkg.dependencies {
436 serde_json::to_string(deps).unwrap_or_default()
437 } else {
438 String::new()
439 };
440
441 conn.execute(
442 "INSERT INTO packages (name, sub_package, repo, version, epoch, \
443 description, package_type, tags, bins, license, registry, scope, \
444 reason, dependencies, revision)
445 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, \
446 ?15)
447 ON CONFLICT(name, sub_package, repo, scope, registry) DO UPDATE SET
448 version = excluded.version,
449 epoch = excluded.epoch,
450 description = excluded.description,
451 package_type = excluded.package_type,
452 tags = excluded.tags,
453 bins = excluded.bins,
454 license = excluded.license,
455 reason = COALESCE(excluded.reason, packages.reason),
456 dependencies = excluded.dependencies,
457 revision = excluded.revision",
458 params![
459 pkg.name,
460 sub_package,
461 pkg.repo,
462 pkg.version,
463 pkg.epoch,
464 pkg.description,
465 pkg_type,
466 tags_json,
467 bins_json,
468 pkg.license,
469 registry,
470 scope_str,
471 reason_str,
472 deps_json,
473 pkg.revision,
474 ]
475 )?;
476
477 let row_id = conn.query_row(
478 "SELECT id FROM packages WHERE name = ?1 AND (sub_package IS ?2) AND \
479 repo = ?3 AND (scope IS ?4 OR (scope IS NULL AND ?4 IS NULL)) AND \
480 (registry IS ?5)",
481 params![pkg.name, sub_package, pkg.repo, scope_str, registry],
482 |row| row.get(0)
483 )?;
484
485 Ok(row_id)
486}
487
488pub fn get_package_id(
494 conn: &Connection,
495 name: &str,
496 sub_package: Option<&str>,
497 repo: &str,
498 registry: &str
499) -> Result<i64> {
500 let id = conn.query_row(
501 "SELECT id FROM packages WHERE name = ?1 AND (sub_package IS ?2) AND \
502 repo = ?3 AND registry = ?4",
503 params![name, sub_package, repo, registry],
504 |row| row.get(0)
505 )?;
506 Ok(id)
507}
508
509pub fn set_package_sizes(
515 conn: &Connection,
516 package_id: i64,
517 archive_size: u64,
518 installed_size: u64
519) -> Result<()> {
520 conn.execute(
521 "UPDATE packages SET archive_size = ?1, installed_size = ?2 WHERE id \
522 = ?3",
523 params![archive_size as i64, installed_size as i64, package_id]
524 )?;
525 Ok(())
526}
527
528pub fn set_package_hash(
534 conn: &Connection,
535 package_id: i64,
536 hash: &str
537) -> Result<()> {
538 conn.execute(
539 "UPDATE packages SET archive_hash = ?1 WHERE id = ?2",
540 params![hash, package_id]
541 )?;
542 Ok(())
543}
544
545pub fn get_package_hash_from_db(
551 registry_handle: &str,
552 name: &str,
553 sub_package: Option<&str>,
554 repo: &str
555) -> Result<Option<String>> {
556 let conn = open_connection(registry_handle)?;
557 let Ok(pkg_id) =
558 get_package_id(&conn, name, sub_package, repo, registry_handle)
559 else {
560 return Ok(None);
561 };
562 let hash: Option<String> = conn
563 .query_row(
564 "SELECT archive_hash FROM packages WHERE id = ?1 AND archive_hash \
565 IS NOT NULL",
566 params![pkg_id],
567 |row| row.get(0)
568 )
569 .ok();
570 Ok(hash)
571}
572
573pub fn get_package_sizes_from_db(
580 registry_handle: &str,
581 name: &str,
582 sub_package: Option<&str>
583) -> Result<Option<(u64, u64)>> {
584 let conn = open_connection(registry_handle)?;
585 let mut stmt = conn.prepare(
586 "SELECT archive_size, installed_size FROM packages WHERE name = ?1 \
587 AND (sub_package IS ?2) AND archive_size IS NOT NULL LIMIT 1"
588 )?;
589 let mut rows = stmt.query(params![name, sub_package])?;
590 if let Some(row) = rows.next()? {
591 let archive: Option<i64> = row.get(0)?;
592 let installed: Option<i64> = row.get(1)?;
593 match (archive, installed) {
594 (Some(a), Some(i)) => {
595 Ok(Some((a.cast_unsigned(), i.cast_unsigned())))
596 }
597 _ => Ok(None)
598 }
599 } else {
600 Ok(None)
601 }
602}
603
604pub fn get_package_files_from_db(
611 registry_handle: &str,
612 name: &str,
613 sub_package: Option<&str>,
614 repo: &str
615) -> Result<Option<Vec<String>>> {
616 let conn = open_connection(registry_handle)?;
617 let Ok(pkg_id) =
618 get_package_id(&conn, name, sub_package, repo, registry_handle)
619 else {
620 return Ok(None);
621 };
622 let mut stmt =
623 conn.prepare("SELECT path FROM package_files WHERE package_id = ?1")?;
624 let rows = stmt.query_map(params![pkg_id], |row| row.get(0))?;
625 let mut files = Vec::new();
626 for row in rows {
627 files.push(row?);
628 }
629 if files.is_empty() {
630 Ok(None)
631 } else {
632 Ok(Some(files))
633 }
634}
635
636#[derive(Debug)]
638pub struct CompletionEntry {
639 pub name: String,
641 pub repo: String,
643 pub description: String,
645 pub sub_package: Option<String>
647}
648
649pub fn get_packages_for_completion(
656 registry_handle: &str
657) -> Result<Vec<CompletionEntry>> {
658 let conn = open_connection(registry_handle)?;
659 let mut stmt = conn.prepare(
660 "SELECT name, repo, description, sub_package FROM packages ORDER BY \
661 name"
662 )?;
663
664 let rows = stmt.query_map([], |row| {
665 Ok(CompletionEntry {
666 name: row.get(0)?,
667 repo: row.get(1)?,
668 description: row.get(2).unwrap_or_default(),
669 sub_package: row.get(3)?
670 })
671 })?;
672
673 let mut entries = Vec::new();
674 for row in rows {
675 entries.push(row?);
676 }
677 Ok(entries)
678}
679
680pub fn update_advisory(
686 conn: &Connection,
687 advisory: &types::Advisory,
688 repo: &str,
689 registry: &str
690) -> Result<()> {
691 let references_json =
692 serde_json::to_string(&advisory.references).unwrap_or_default();
693 let severity_str = format!("{:?}", advisory.severity).to_lowercase();
694
695 conn.execute(
696 "INSERT INTO package_advisories (id, package, sub_package, summary, \
697 severity, cvss, affected_range, fixed_in, description, \
698 references_json, repo, registry)
699 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
700 ON CONFLICT(id) DO UPDATE SET
701 package = excluded.package,
702 sub_package = excluded.sub_package,
703 summary = excluded.summary,
704 severity = excluded.severity,
705 cvss = excluded.cvss,
706 affected_range = excluded.affected_range,
707 fixed_in = excluded.fixed_in,
708 description = excluded.description,
709 references_json = excluded.references_json,
710 repo = excluded.repo,
711 registry = excluded.registry",
712 params![
713 advisory.id,
714 advisory.package,
715 advisory.sub_package,
716 advisory.summary,
717 severity_str,
718 advisory.cvss,
719 advisory.affected_range,
720 advisory.fixed_in,
721 advisory.description,
722 references_json,
723 repo,
724 registry,
725 ]
726 )?;
727 Ok(())
728}
729
730pub fn list_all_advisories(
737 registry_handle: &str
738) -> Result<Vec<(types::Advisory, String)>> {
739 let conn = open_connection(registry_handle)?;
740 let mut stmt = conn.prepare(
741 "SELECT id, package, sub_package, summary, severity, cvss, \
742 affected_range, fixed_in, description, references_json, repo FROM \
743 package_advisories"
744 )?;
745
746 let rows = stmt.query_map([], |row| {
747 let severity_raw: String = row.get(4)?;
748 let severity = match severity_raw.as_str() {
749 "medium" => types::Severity::Medium,
750 "high" => types::Severity::High,
751 "critical" => types::Severity::Critical,
752 _ => types::Severity::Low
753 };
754
755 let references_raw: String = row.get(9)?;
756 let references: Option<Vec<String>> =
757 serde_json::from_str(&references_raw).ok();
758
759 Ok((
760 types::Advisory {
761 id: row.get(0)?,
762 package: row.get(1)?,
763 sub_package: row.get(2)?,
764 summary: row.get(3)?,
765 severity,
766 cvss: row.get(5)?,
767 affected_range: row.get(6)?,
768 fixed_in: row.get(7)?,
769 description: row.get(8)?,
770 references
771 },
772 row.get::<_, String>(10)?
773 ))
774 })?;
775
776 let mut advisories = Vec::new();
777 for row in rows {
778 advisories.push(row?);
779 }
780 Ok(advisories)
781}
782
783pub fn get_advisories_for_package(
790 registry_handle: &str,
791 package_name: &str,
792 sub_package: Option<&str>
793) -> Result<Vec<types::Advisory>> {
794 let conn = open_connection(registry_handle)?;
795
796 let (query, params_vec): (String, Vec<rusqlite::types::Value>) =
797 match sub_package {
798 Some(sub) => (
799 "SELECT id, package, sub_package, summary, severity, cvss, \
800 affected_range, fixed_in, description, references_json
801 FROM package_advisories
802 WHERE package = ?1 AND (sub_package IS ?2 OR sub_package IS NULL)"
803 .to_string(),
804 vec![package_name.to_string().into(), sub.to_string().into()]
805 ),
806 None => (
807 "SELECT id, package, sub_package, summary, severity, cvss, \
808 affected_range, fixed_in, description, references_json
809 FROM package_advisories
810 WHERE package = ?1 AND sub_package IS NULL"
811 .to_string(),
812 vec![package_name.to_string().into()]
813 )
814 };
815
816 let mut stmt = conn.prepare(&query)?;
817
818 let rows =
819 stmt.query_map(rusqlite::params_from_iter(params_vec), |row| {
820 let severity_raw: String = row.get(4)?;
821 let severity = match severity_raw.as_str() {
822 "medium" => types::Severity::Medium,
823 "high" => types::Severity::High,
824 "critical" => types::Severity::Critical,
825 _ => types::Severity::Low
826 };
827
828 let references_raw: String = row.get(9)?;
829 let references: Option<Vec<String>> =
830 serde_json::from_str(&references_raw).ok();
831
832 Ok(types::Advisory {
833 id: row.get(0)?,
834 package: row.get(1)?,
835 sub_package: row.get(2)?,
836 summary: row.get(3)?,
837 severity,
838 cvss: row.get(5)?,
839 affected_range: row.get(6)?,
840 fixed_in: row.get(7)?,
841 description: row.get(8)?,
842 references
843 })
844 })?;
845
846 let mut advisories = Vec::new();
847 for row in rows {
848 advisories.push(row?);
849 }
850 Ok(advisories)
851}
852
853pub fn index_package_files(
859 conn: &Connection,
860 package_id: i64,
861 files: &[String]
862) -> Result<()> {
863 let mut stmt = conn.prepare(
864 "INSERT INTO package_files (package_id, path) VALUES (?1, ?2)"
865 )?;
866 for file in files {
867 stmt.execute(params![package_id, file])?;
868 }
869 Ok(())
870}
871
872pub fn clear_package_files(conn: &Connection, package_id: i64) -> Result<()> {
878 conn.execute(
879 "DELETE FROM package_files WHERE package_id = ?1",
880 params![package_id]
881 )?;
882 Ok(())
883}
884
885pub fn has_other_owners(
891 conn: &Connection,
892 path: &str,
893 current_package_id: i64
894) -> Result<bool> {
895 let count: i64 = conn.query_row(
896 "SELECT count(*) FROM package_files WHERE path = ?1 AND package_id != \
897 ?2",
898 params![path, current_package_id],
899 |row| row.get(0)
900 )?;
901 Ok(count > 0)
902}
903
904pub fn delete_package(
910 conn: &Connection,
911 name: &str,
912 sub_package: Option<&str>,
913 repo: &str,
914 scope: Option<types::Scope>
915) -> Result<()> {
916 let scope_str = scope.map(|s| format!("{s:?}").to_lowercase());
917 conn.execute(
918 "DELETE FROM packages WHERE name = ?1 AND (sub_package IS ?2) AND \
919 repo = ?3 AND (scope IS ?4 OR scope IS NULL)",
920 params![name, sub_package, repo, scope_str]
921 )?;
922 Ok(())
923}
924
925pub fn clear_registry(conn: &Connection) -> Result<()> {
931 conn.execute("DELETE FROM packages", [])?;
932 conn.execute("DELETE FROM package_advisories", [])?;
933 Ok(())
934}
935
936pub fn find_provides(
942 registry_handle: &str,
943 term: &str
944) -> Result<Vec<(types::Package, String)>> {
945 let conn = open_connection(registry_handle)?;
946
947 let mut stmt = conn.prepare(
948 "SELECT name, repo, version, description, package_type, tags, bins, \
949 license, sub_package, revision, epoch
950 FROM packages
951 WHERE name = ?1"
952 )?;
953
954 let rows = stmt.query_map(params![term], |row| {
955 let tags_raw: String = row.get(5)?;
956 let tags: Vec<String> =
957 serde_json::from_str(&tags_raw).unwrap_or_default();
958 let bins_raw: String = row.get::<_, String>(6).unwrap_or_default();
959 let bins: Vec<String> =
960 serde_json::from_str(&bins_raw).unwrap_or_default();
961 let type_raw: String = row.get(4)?;
962 let revision: String = row.get(9).unwrap_or_else(|_| "1".to_string());
963 let epoch: u32 = row.get(10).unwrap_or(0);
964
965 let package_type = match type_raw.as_str() {
966 "collection" => types::PackageType::Collection,
967 "app" => types::PackageType::App,
968 "extension" => types::PackageType::Extension,
969 _ => types::PackageType::Package
970 };
971
972 Ok((
973 types::Package {
974 name: row.get(0)?,
975 repo: row.get(1)?,
976 version: row.get(2)?,
977 epoch,
978 revision,
979 description: row.get(3)?,
980 package_type,
981 tags,
982 bins: Some(bins.clone()),
983 license: row.get(7)?,
984 sub_package: row.get(8)?,
985 maintainer: types::Maintainer::default(),
986 ..Default::default()
987 },
988 bins
989 ))
990 })?;
991
992 let mut results = Vec::new();
993 for row in rows {
994 let (pkg, bins) = row?;
995 if bins.is_empty() {
996 results.push((pkg, format!("bin/{term}")));
997 } else {
998 for bin in &bins {
999 results.push((pkg.clone(), format!("bin/{bin}")));
1000 }
1001 }
1002 }
1003
1004 let mut stmt = conn.prepare(
1005 "SELECT name, repo, version, description, package_type, tags, bins, \
1006 license, sub_package, revision, epoch
1007 FROM packages
1008 WHERE bins IS NOT NULL"
1009 )?;
1010
1011 let rows = stmt.query_map([], |row| {
1012 let tags_raw: String = row.get(5)?;
1013 let tags: Vec<String> =
1014 serde_json::from_str(&tags_raw).unwrap_or_default();
1015 let bins_raw: String = row.get(6)?;
1016 let bins: Vec<String> =
1017 serde_json::from_str(&bins_raw).unwrap_or_default();
1018 let type_raw: String = row.get(4)?;
1019 let revision: String = row.get(9).unwrap_or_else(|_| "1".to_string());
1020 let epoch: u32 = row.get(10).unwrap_or(0);
1021
1022 let package_type = match type_raw.as_str() {
1023 "collection" => types::PackageType::Collection,
1024 "app" => types::PackageType::App,
1025 "extension" => types::PackageType::Extension,
1026 _ => types::PackageType::Package
1027 };
1028
1029 Ok(types::Package {
1030 name: row.get(0)?,
1031 repo: row.get(1)?,
1032 version: row.get(2)?,
1033 epoch,
1034 revision,
1035 description: row.get(3)?,
1036 package_type,
1037 tags,
1038 bins: Some(bins),
1039 license: row.get(7)?,
1040 sub_package: row.get(8)?,
1041 maintainer: types::Maintainer::default(),
1042 ..Default::default()
1043 })
1044 })?;
1045
1046 for row in rows {
1047 let pkg = row?;
1048 if let Some(bins) = &pkg.bins {
1049 for bin in bins {
1050 if bin == term || bin.contains(term) {
1051 results.push((pkg.clone(), format!("bin/{bin}")));
1052 }
1053 }
1054 }
1055 }
1056
1057 let mut stmt = conn.prepare(
1058 "SELECT p.name, p.repo, p.version, p.description, p.package_type, \
1059 p.tags, p.bins, p.license, p.sub_package, pf.path, p.revision, \
1060 p.epoch
1061 FROM packages p
1062 JOIN package_files pf ON p.id = pf.package_id
1063 WHERE pf.path LIKE ?1 OR pf.path LIKE ?2"
1064 )?;
1065
1066 let path_like_query = format!("%/{term}");
1067 let exact_path_query = term.to_string();
1068
1069 let rows =
1070 stmt.query_map(params![path_like_query, exact_path_query], |row| {
1071 let tags_raw: String = row.get(5)?;
1072 let tags: Vec<String> =
1073 serde_json::from_str(&tags_raw).unwrap_or_default();
1074 let bins_raw: String = row.get(6)?;
1075 let bins: Vec<String> =
1076 serde_json::from_str(&bins_raw).unwrap_or_default();
1077 let type_raw: String = row.get(4)?;
1078 let revision: String =
1079 row.get(10).unwrap_or_else(|_| "1".to_string());
1080 let epoch: u32 = row.get(11).unwrap_or(0);
1081
1082 let package_type = match type_raw.as_str() {
1083 "collection" => types::PackageType::Collection,
1084 "app" => types::PackageType::App,
1085 "extension" => types::PackageType::Extension,
1086 _ => types::PackageType::Package
1087 };
1088
1089 let pkg = types::Package {
1090 name: row.get(0)?,
1091 repo: row.get(1)?,
1092 version: row.get(2)?,
1093 epoch,
1094 revision,
1095 description: row.get(3)?,
1096 package_type,
1097 tags,
1098 bins: Some(bins),
1099 license: row.get(7)?,
1100 sub_package: row.get(8)?,
1101 maintainer: types::Maintainer::default(),
1102 ..Default::default()
1103 };
1104 let path: String = row.get(9)?;
1105 Ok((pkg, path))
1106 })?;
1107
1108 for row in rows {
1109 let (pkg, mut path): (types::Package, String) = row?;
1110 if let Some(stripped) = path.strip_prefix("data/pkgstore/") {
1111 path = format!("/{stripped}");
1112 } else if let Some(stripped) = path.strip_prefix("data/home/") {
1113 path = format!("~/{stripped}");
1114 }
1115
1116 results.push((pkg, path));
1117 }
1118
1119 results.sort_by(|a, b| {
1120 a.0.name
1121 .cmp(&b.0.name)
1122 .then(a.0.repo.cmp(&b.0.repo))
1123 .then(a.1.cmp(&b.1))
1124 });
1125 results.dedup_by(|a, b| {
1126 a.0.name == b.0.name && a.0.repo == b.0.repo && a.1 == b.1
1127 });
1128
1129 Ok(results)
1130}
1131
1132pub fn search_packages(
1139 registry_handle: &str,
1140 term: &str
1141) -> Result<Vec<types::Package>> {
1142 let conn = open_connection(registry_handle)?;
1143 let mut stmt = conn.prepare(
1144 "SELECT name, repo, version, description, package_type, tags, \
1145 license, sub_package, revision, epoch
1146 FROM packages
1147 WHERE id IN (SELECT rowid FROM packages_fts WHERE packages_fts MATCH \
1148 ?1)
1149 OR name LIKE ?2"
1150 )?;
1151
1152 let search_query = format!("{term}*");
1153 let like_query = format!("%{term}%");
1154
1155 let rows = stmt.query_map(params![search_query, like_query], |row| {
1156 let tags_raw: String = row.get(5)?;
1157 let tags: Vec<String> =
1158 serde_json::from_str(&tags_raw).unwrap_or_default();
1159 let type_raw: String = row.get(4)?;
1160 let revision: String = row.get(8).unwrap_or_else(|_| "1".to_string());
1161 let epoch: u32 = row.get(9).unwrap_or(0);
1162
1163 let package_type = match type_raw.as_str() {
1164 "collection" => types::PackageType::Collection,
1165 "app" => types::PackageType::App,
1166 "extension" => types::PackageType::Extension,
1167 _ => types::PackageType::Package
1168 };
1169
1170 Ok(types::Package {
1171 name: row.get(0)?,
1172 repo: row.get(1)?,
1173 version: row.get(2)?,
1174 epoch,
1175 revision,
1176 description: row.get(3)?,
1177 package_type,
1178 tags,
1179 license: row.get(6)?,
1180 sub_package: row.get(7)?,
1181 maintainer: types::Maintainer {
1182 name: String::new(),
1183 email: String::new(),
1184 website: None
1185 },
1186 ..Default::default()
1187 })
1188 })?;
1189
1190 let mut pkgs = Vec::new();
1191 for row in rows {
1192 pkgs.push(row?);
1193 }
1194 Ok(pkgs)
1195}
1196
1197pub fn search_files(
1204 registry_handle: &str,
1205 term: &str
1206) -> Result<Vec<(types::Package, String)>> {
1207 let conn = open_connection(registry_handle)?;
1208 let like_query = format!("%{term}%");
1209
1210 let has_fts = conn
1211 .prepare("SELECT 1 FROM package_files_fts LIMIT 0")
1212 .is_ok();
1213
1214 macro_rules! map_file_row {
1215 ($row:expr) => {{
1216 let tags_raw: String = $row.get(5)?;
1217 let tags: Vec<String> =
1218 serde_json::from_str(&tags_raw).unwrap_or_default();
1219 let type_raw: String = $row.get(4)?;
1220 let revision: String =
1221 $row.get(9).unwrap_or_else(|_| "1".to_string());
1222 let epoch: u32 = $row.get(10).unwrap_or(0);
1223 let package_type = match type_raw.as_str() {
1224 "collection" => types::PackageType::Collection,
1225 "app" => types::PackageType::App,
1226 "extension" => types::PackageType::Extension,
1227 _ => types::PackageType::Package
1228 };
1229 let pkg = types::Package {
1230 name: $row.get(0)?,
1231 repo: $row.get(1)?,
1232 version: $row.get(2)?,
1233 epoch,
1234 revision,
1235 description: $row.get(3)?,
1236 package_type,
1237 tags,
1238 license: $row.get(6)?,
1239 sub_package: $row.get(7)?,
1240 maintainer: types::Maintainer {
1241 name: String::new(),
1242 email: String::new(),
1243 website: None
1244 },
1245 ..Default::default()
1246 };
1247 let path: String = $row.get(8)?;
1248 Ok::<_, rusqlite::Error>((pkg, path))
1249 }};
1250 }
1251
1252 if has_fts {
1253 let search_query = term
1254 .replace('/', " ")
1255 .split_whitespace()
1256 .collect::<Vec<_>>()
1257 .join("* ");
1258 let mut stmt = conn.prepare(
1259 "SELECT p.name, p.repo, p.version, p.description, p.package_type, \
1260 p.tags, p.license, p.sub_package, pf.path, p.revision, p.epoch
1261 FROM packages p
1262 JOIN package_files pf ON p.id = pf.package_id
1263 WHERE pf.id IN (SELECT rowid FROM package_files_fts WHERE \
1264 package_files_fts MATCH ?1)
1265 OR pf.path LIKE ?2"
1266 )?;
1267 let rows = stmt
1268 .query_map(params![search_query, like_query], |row| {
1269 map_file_row!(row)
1270 })?;
1271 let mut results = Vec::new();
1272 for row in rows {
1273 results.push(row?);
1274 }
1275 Ok(results)
1276 } else {
1277 let mut stmt = conn.prepare(
1278 "SELECT p.name, p.repo, p.version, p.description, p.package_type, \
1279 p.tags, p.license, p.sub_package, pf.path, p.revision, p.epoch
1280 FROM packages p
1281 JOIN package_files pf ON p.id = pf.package_id
1282 WHERE pf.path LIKE ?1"
1283 )?;
1284 let rows =
1285 stmt.query_map(params![like_query], |row| map_file_row!(row))?;
1286 let mut results = Vec::new();
1287 for row in rows {
1288 results.push(row?);
1289 }
1290 Ok(results)
1291 }
1292}
1293
1294pub fn list_all_packages(registry_handle: &str) -> Result<Vec<types::Package>> {
1301 let conn = open_connection(registry_handle)?;
1302 let mut stmt = conn.prepare(
1303 "SELECT name, repo, version, description, package_type, tags, \
1304 license, sub_package, scope, registry, reason, revision, epoch FROM \
1305 packages ORDER BY name"
1306 )?;
1307
1308 let rows = stmt.query_map([], |row| {
1309 let tags_raw: String = row.get(5)?;
1310 let tags: Vec<String> =
1311 serde_json::from_str(&tags_raw).unwrap_or_default();
1312 let type_raw: String = row.get(4)?;
1313
1314 let package_type = match type_raw.as_str() {
1315 "collection" => types::PackageType::Collection,
1316 "app" => types::PackageType::App,
1317 "extension" => types::PackageType::Extension,
1318 _ => types::PackageType::Package
1319 };
1320
1321 let sub_package: Option<String> = row.get(7)?;
1322 let scope_raw: Option<String> = row.get(8)?;
1323 let registry: Option<String> = row.get(9)?;
1324 let reason_raw: Option<String> = row.get(10)?;
1325 let revision: String = row.get(11).unwrap_or_else(|_| "1".to_string());
1326 let epoch: u32 = row.get(12).unwrap_or(0);
1327
1328 let scope = match scope_raw.as_deref() {
1329 Some("system") => types::Scope::System,
1330 Some("project") => types::Scope::Project,
1331 _ => types::Scope::User
1332 };
1333
1334 let reason = reason_raw.map(|r| {
1335 if r == "direct" {
1336 types::InstallReason::Direct
1337 } else if let Some(parent) = r.strip_prefix("dependency:") {
1338 types::InstallReason::Dependency {
1339 parent: parent.to_string()
1340 }
1341 } else {
1342 types::InstallReason::Direct
1343 }
1344 });
1345
1346 let pkg = types::Package {
1347 name: row.get(0)?,
1348 repo: row.get(1)?,
1349 version: row.get(2)?,
1350 epoch,
1351 revision,
1352 description: row.get(3)?,
1353 package_type,
1354 tags,
1355 license: row.get(6)?,
1356 scope,
1357 registry_handle: registry,
1358 sub_package,
1359 reason,
1360 maintainer: types::Maintainer {
1361 name: String::new(),
1362 email: String::new(),
1363 website: None
1364 },
1365 ..Default::default()
1366 };
1367
1368 Ok(pkg)
1369 })?;
1370
1371 let mut pkgs = Vec::new();
1372 for row in rows {
1373 pkgs.push(row?);
1374 }
1375 Ok(pkgs)
1376}
1377
1378pub fn get_all_versions(
1385 registry_handle: &str,
1386 name: &str,
1387 repo: &str
1388) -> Result<Vec<String>> {
1389 let conn = open_connection(registry_handle)?;
1390 let mut stmt = conn.prepare(
1391 "SELECT version FROM packages WHERE name = ?1 AND repo = ?2"
1392 )?;
1393 let rows = stmt.query_map(params![name, repo], |row| row.get(0))?;
1394 let mut versions = Vec::new();
1395 for v in rows.flatten() {
1396 versions.push(v);
1397 }
1398 Ok(versions)
1399}
1400
1401pub fn get_package_dependencies(
1408 registry_handle: &str,
1409 name: &str,
1410 version: &str,
1411 sub_package: Option<&str>,
1412 repo: &str
1413) -> Result<Option<String>> {
1414 let conn = open_connection(registry_handle)?;
1415 let mut stmt = conn.prepare(
1416 "SELECT dependencies FROM packages
1417 WHERE name = ?1 AND version = ?2 AND (sub_package IS ?3) AND repo = ?4",
1418 )?;
1419 let mut rows = stmt.query(params![name, version, sub_package, repo])?;
1420 if let Some(row) = rows.next()? {
1421 let deps: Option<String> = row.get(0)?;
1422 Ok(deps)
1423 } else {
1424 Ok(None)
1425 }
1426}
1427
1428pub fn get_dependents(
1435 registry_handle: &str,
1436 target_package_name: &str,
1437 target_sub_package: Option<&str>
1438) -> Result<Vec<types::Package>> {
1439 let conn = open_connection(registry_handle)?;
1440 let mut stmt = conn.prepare(
1441 "SELECT name, repo, version, description, package_type, tags, \
1442 license, sub_package, scope, registry, reason, revision, epoch, \
1443 dependencies FROM packages"
1444 )?;
1445
1446 let rows = stmt.query_map([], |row| {
1447 let dependencies_raw: Option<String> = row.get(13)?;
1448 let name: String = row.get(0)?;
1449
1450 let mut depends_on_target = false;
1451 if let Some(deps_json) = dependencies_raw
1452 && deps_json.contains(target_package_name)
1453 {
1454 if let Ok(deps) =
1456 serde_json::from_str::<types::Dependencies>(&deps_json)
1457 {
1458 let resolved = deps.resolve(
1459 &[],
1460 &[],
1461 row.get::<_, Option<String>>(7)?.as_deref(),
1462 true,
1463 None
1464 );
1465 let all_deps = [
1466 resolved.runtime,
1467 resolved.test,
1468 resolved
1469 .build
1470 .into_iter()
1471 .flat_map(|b| b.packages)
1472 .collect()
1473 ]
1474 .concat();
1475
1476 for dep_str in all_deps {
1477 if let Ok(dep) = zoi_deps::parse_dependency_string(&dep_str)
1478 && dep.manager == "zoi"
1479 && let Ok(req) =
1480 zoi_resolver::resolve::parse_source_string(
1481 dep.package
1482 )
1483 && req.name == target_package_name
1484 && req.sub_package.as_deref() == target_sub_package
1485 {
1486 depends_on_target = true;
1487 break;
1488 }
1489 }
1490 }
1491 }
1492
1493 if !depends_on_target {
1494 return Err(rusqlite::Error::QueryReturnedNoRows);
1495 }
1496
1497 let tags_raw: String = row.get(5)?;
1498 let tags: Vec<String> =
1499 serde_json::from_str(&tags_raw).unwrap_or_default();
1500 let type_raw: String = row.get(4)?;
1501 let package_type = match type_raw.as_str() {
1502 "collection" => types::PackageType::Collection,
1503 "app" => types::PackageType::App,
1504 "extension" => types::PackageType::Extension,
1505 _ => types::PackageType::Package
1506 };
1507
1508 let scope_raw: Option<String> = row.get(8)?;
1509 let scope = match scope_raw.as_deref() {
1510 Some("system") => types::Scope::System,
1511 Some("project") => types::Scope::Project,
1512 _ => types::Scope::User
1513 };
1514
1515 Ok(types::Package {
1516 name,
1517 repo: row.get(1)?,
1518 version: row.get(2)?,
1519 description: row.get(3)?,
1520 package_type,
1521 tags,
1522 license: row.get(6)?,
1523 sub_package: row.get(7)?,
1524 scope,
1525 registry_handle: row.get(9)?,
1526 revision: row.get(11).unwrap_or_else(|_| "1".to_string()),
1527 epoch: row.get(12).unwrap_or(0),
1528 ..Default::default()
1529 })
1530 })?;
1531
1532 let mut pkgs = Vec::new();
1533 for pkg in rows.flatten() {
1534 pkgs.push(pkg);
1535 }
1536 Ok(pkgs)
1537}