Skip to main content

zoi_db/
lib.rs

1//! Database management for Zoi registry metadata.
2//!
3//! This crate provides functionality to manage the SQLite-based registry
4//! metadata, including package information, file indexing, and security
5//! advisories.
6
7use 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
14/// Gets the path to the database for a specific registry.
15///
16/// # Errors
17///
18/// Returns an error if the database root directory cannot be determined.
19pub 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        // Fallback to host metadata for bootstrapping
25        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            // Copy host registry DB to target so records are kept in target
33            let _ = std::fs::copy(&host_path, &target_path);
34        }
35    }
36
37    Ok(target_path)
38}
39
40/// Opens a connection to the registry database and ensures the schema is set
41/// up.
42///
43/// # Errors
44///
45/// Returns an error if the database cannot be opened or if the schema setup
46/// fails.
47pub 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
53/// Opens a raw `SQLite` connection and configures high-performance pragmas.
54///
55/// Performance Tuning:
56/// - `WAL` (Write-Ahead Logging): Allows concurrent readers and a single
57///   writer.
58/// - `NORMAL` Synchronous: Balanced safety and speed for registry metadata.
59///
60/// # Errors
61///
62/// Returns an error if the database cannot be opened or if PRAGMA configuration
63/// fails.
64pub 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
83/// Initializes or migrates the Zoi registry metadata schema.
84///
85/// Schema Highlights:
86/// - `packages`: The main metadata store with FTS5 search indexing.
87/// - `package_files`: Index of every file path provided by every package (used
88///   by `zoi provides`).
89/// - `package_advisories`: Security vulnerability database.
90///
91/// FTS5 Search: Uses `SQLite`'s virtual tables for sub-millisecond full-text
92/// search across thousands of package descriptions and tags.
93fn 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
409/// Updates or inserts a package in the database.
410///
411/// # Errors
412///
413/// Returns an error if serialization fails or if the database execution fails.
414pub 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
488/// Retrieves the internal database ID for a package.
489///
490/// # Errors
491///
492/// Returns an error if the query fails or if the package is not found.
493pub 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
509/// Updates the archive and installed sizes for a package.
510///
511/// # Errors
512///
513/// Returns an error if the database update fails.
514pub 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
528/// Updates the archive hash for a package.
529///
530/// # Errors
531///
532/// Returns an error if the database update fails.
533pub 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
545/// Retrieves the archive hash for a package from the database.
546///
547/// # Errors
548///
549/// Returns an error if the database connection cannot be opened.
550pub 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
573/// Retrieves the archive and installed sizes for a package from the database.
574///
575/// # Errors
576///
577/// Returns an error if the database connection cannot be opened or the query
578/// fails.
579pub 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
604/// Retrieves the list of files associated with a package from the database.
605///
606/// # Errors
607///
608/// Returns an error if the database connection cannot be opened or the query
609/// fails.
610pub 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/// A single entry for shell completion.
637#[derive(Debug)]
638pub struct CompletionEntry {
639    /// The name of the package.
640    pub name: String,
641    /// The repository where the package is located.
642    pub repo: String,
643    /// A short description of the package.
644    pub description: String,
645    /// The name of the sub-package, if applicable.
646    pub sub_package: Option<String>
647}
648
649/// Retrieves a list of packages suitable for shell completion.
650///
651/// # Errors
652///
653/// Returns an error if the database connection cannot be opened or the query
654/// fails.
655pub 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
680/// Updates or inserts a security advisory in the database.
681///
682/// # Errors
683///
684/// Returns an error if serialization fails or if the database execution fails.
685pub 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
730/// Lists all security advisories in the database.
731///
732/// # Errors
733///
734/// Returns an error if the database connection cannot be opened or the query
735/// fails.
736pub 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
783/// Retrieves all security advisories for a specific package.
784///
785/// # Errors
786///
787/// Returns an error if the database connection cannot be opened or the query
788/// fails.
789pub 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
853/// Indexes the files provided by a package.
854///
855/// # Errors
856///
857/// Returns an error if the database execution fails.
858pub 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
872/// Clears the file index for a specific package.
873///
874/// # Errors
875///
876/// Returns an error if the database execution fails.
877pub 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
885/// Checks if a file path is owned by any package other than the specified one.
886///
887/// # Errors
888///
889/// Returns an error if the query fails.
890pub 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
904/// Deletes a package from the database.
905///
906/// # Errors
907///
908/// Returns an error if the database execution fails.
909pub 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
925/// Clears all package and advisory data from the database.
926///
927/// # Errors
928///
929/// Returns an error if the database execution fails.
930pub fn clear_registry(conn: &Connection) -> Result<()> {
931    conn.execute("DELETE FROM packages", [])?;
932    conn.execute("DELETE FROM package_advisories", [])?;
933    Ok(())
934}
935
936/// Finds packages that provide a specific command or file.
937///
938/// # Errors
939///
940/// Returns an error if the database connection or query fails.
941pub 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
1132/// Searches for packages matching a search term.
1133///
1134/// # Errors
1135///
1136/// Returns an error if the database connection cannot be opened or the query
1137/// fails.
1138pub 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
1197/// Searches for files matching a search term.
1198///
1199/// # Errors
1200///
1201/// Returns an error if the database connection cannot be opened or the query
1202/// fails.
1203pub 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
1294/// Lists all packages in the database.
1295///
1296/// # Errors
1297///
1298/// Returns an error if the database connection cannot be opened or the query
1299/// fails.
1300pub 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
1378/// Retrieves all versions of a specific package from the database.
1379///
1380/// # Errors
1381///
1382/// Returns an error if the database connection cannot be opened or the query
1383/// fails.
1384pub 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
1401/// Retrieves the dependencies of a specific package version.
1402///
1403/// # Errors
1404///
1405/// Returns an error if the database connection cannot be opened or the query
1406/// fails.
1407pub 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
1428/// Finds all installed packages that depend on a specific package.
1429///
1430/// # Errors
1431///
1432/// Returns an error if the database connection cannot be opened or the query
1433/// fails.
1434pub 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            // Heuristic check first for performance
1455            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}