1use crate::build::{BuildOutcome, BuildResult};
34use anyhow::{Context, Result};
35use pkgsrc::{PkgName, PkgPath, ScanIndex};
36use rusqlite::{Connection, params};
37use std::collections::HashSet;
38use std::path::Path;
39use std::time::Duration;
40use tracing::{debug, info, warn};
41
42const SCHEMA_VERSION: i32 = 2;
44
45#[derive(Clone, Debug)]
47pub struct PackageRow {
48 pub id: i64,
49 pub pkgname: String,
50 pub pkgpath: String,
51 pub skip_reason: Option<String>,
52 pub fail_reason: Option<String>,
53 pub is_bootstrap: bool,
54 pub pbulk_weight: i32,
55}
56
57pub struct Database {
59 conn: Connection,
60}
61
62impl Database {
63 pub fn open(path: &Path) -> Result<Self> {
65 if let Some(parent) = path.parent() {
66 std::fs::create_dir_all(parent)
67 .context("Failed to create database directory")?;
68 }
69 let conn = Connection::open(path).context("Failed to open database")?;
70 let db = Self { conn };
71 db.configure_pragmas()?;
72 db.init_or_migrate()?;
73 Ok(db)
74 }
75
76 fn configure_pragmas(&self) -> Result<()> {
78 self.conn.execute_batch(
79 "PRAGMA journal_mode = WAL;
80 PRAGMA synchronous = NORMAL;
81 PRAGMA cache_size = -64000;
82 PRAGMA temp_store = MEMORY;
83 PRAGMA mmap_size = 268435456;
84 PRAGMA foreign_keys = ON;",
85 )?;
86 Ok(())
87 }
88
89 fn init_or_migrate(&self) -> Result<()> {
91 let has_schema_version: bool = self.conn.query_row(
93 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_version'",
94 [],
95 |row| row.get::<_, i32>(0).map(|c| c > 0),
96 )?;
97
98 if !has_schema_version {
99 let has_old_scan: bool = self.conn.query_row(
101 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='scan'",
102 [],
103 |row| row.get::<_, i32>(0).map(|c| c > 0),
104 )?;
105
106 if has_old_scan {
107 info!("Migrating from schema v1 to v{}", SCHEMA_VERSION);
108 self.migrate_v1_to_v2()?;
109 } else {
110 self.create_schema_v2()?;
111 }
112 } else {
113 let version: i32 = self.conn.query_row(
114 "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1",
115 [],
116 |row| row.get(0),
117 ).unwrap_or(1);
118
119 if version < SCHEMA_VERSION {
120 info!(
121 "Migrating from schema v{} to v{}",
122 version, SCHEMA_VERSION
123 );
124 if version == 1 {
125 self.migrate_v1_to_v2()?;
126 }
127 }
128 }
129
130 Ok(())
131 }
132
133 fn create_schema_v2(&self) -> Result<()> {
135 self.conn.execute_batch(
136 "CREATE TABLE IF NOT EXISTS schema_version (
137 version INTEGER PRIMARY KEY
138 );
139
140 INSERT OR REPLACE INTO schema_version (version) VALUES (2);
141
142 CREATE TABLE IF NOT EXISTS packages (
143 id INTEGER PRIMARY KEY AUTOINCREMENT,
144 pkgname TEXT UNIQUE NOT NULL,
145 pkgpath TEXT NOT NULL,
146 pkgname_base TEXT NOT NULL,
147 version TEXT NOT NULL,
148 skip_reason TEXT,
149 fail_reason TEXT,
150 is_bootstrap INTEGER DEFAULT 0,
151 pbulk_weight INTEGER DEFAULT 100,
152 scan_data TEXT,
153 scanned_at INTEGER NOT NULL
154 );
155
156 CREATE INDEX IF NOT EXISTS idx_packages_pkgpath ON packages(pkgpath);
157 CREATE INDEX IF NOT EXISTS idx_packages_pkgname_base ON packages(pkgname_base);
158 CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(skip_reason, fail_reason);
159
160 CREATE TABLE IF NOT EXISTS depends (
161 id INTEGER PRIMARY KEY AUTOINCREMENT,
162 package_id INTEGER NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
163 depend_pattern TEXT NOT NULL,
164 depend_pkgpath TEXT NOT NULL,
165 UNIQUE(package_id, depend_pattern)
166 );
167
168 CREATE INDEX IF NOT EXISTS idx_depends_package ON depends(package_id);
169 CREATE INDEX IF NOT EXISTS idx_depends_pkgpath ON depends(depend_pkgpath);
170
171 CREATE TABLE IF NOT EXISTS resolved_depends (
172 id INTEGER PRIMARY KEY AUTOINCREMENT,
173 package_id INTEGER NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
174 depends_on_id INTEGER NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
175 UNIQUE(package_id, depends_on_id)
176 );
177
178 CREATE INDEX IF NOT EXISTS idx_resolved_depends_package ON resolved_depends(package_id);
179 CREATE INDEX IF NOT EXISTS idx_resolved_depends_depends_on ON resolved_depends(depends_on_id);
180
181 CREATE TABLE IF NOT EXISTS builds (
182 id INTEGER PRIMARY KEY AUTOINCREMENT,
183 package_id INTEGER NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
184 outcome TEXT NOT NULL,
185 outcome_detail TEXT,
186 duration_ms INTEGER NOT NULL DEFAULT 0,
187 built_at INTEGER NOT NULL,
188 log_dir TEXT,
189 UNIQUE(package_id)
190 );
191
192 CREATE INDEX IF NOT EXISTS idx_builds_outcome ON builds(outcome);
193 CREATE INDEX IF NOT EXISTS idx_builds_package ON builds(package_id);
194
195 CREATE TABLE IF NOT EXISTS metadata (
196 key TEXT PRIMARY KEY,
197 value TEXT NOT NULL
198 );"
199 )?;
200
201 debug!("Created schema v2");
202 Ok(())
203 }
204
205 fn migrate_v1_to_v2(&self) -> Result<()> {
207 self.create_schema_v2()?;
209
210 let mut stmt = self
212 .conn
213 .prepare("SELECT pkgpath, data FROM scan ORDER BY rowid")?;
214
215 let rows: Vec<(String, String)> = stmt
216 .query_map([], |row| {
217 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
218 })?
219 .filter_map(|r| r.ok())
220 .collect();
221
222 let mut migrated_count = 0;
223 for (pkgpath, json) in rows {
224 let indexes: Vec<ScanIndex> = match serde_json::from_str(&json) {
225 Ok(idx) => idx,
226 Err(e) => {
227 warn!(pkgpath = %pkgpath, error = %e, "Failed to parse scan data during migration");
228 continue;
229 }
230 };
231
232 for idx in indexes {
233 if let Err(e) = self.store_package(&pkgpath, &idx) {
234 warn!(pkgpath = %pkgpath, error = %e, "Failed to migrate package");
235 }
236 }
237 migrated_count += 1;
238 }
239
240 let mut stmt = self
242 .conn
243 .prepare("SELECT pkgname, data FROM build ORDER BY rowid")?;
244
245 let rows: Vec<(String, String)> = stmt
246 .query_map([], |row| {
247 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
248 })?
249 .filter_map(|r| r.ok())
250 .collect();
251
252 let mut build_count = 0;
253 for (pkgname, json) in rows {
254 let result: BuildResult = match serde_json::from_str(&json) {
255 Ok(r) => r,
256 Err(e) => {
257 warn!(pkgname = %pkgname, error = %e, "Failed to parse build data during migration");
258 continue;
259 }
260 };
261
262 if let Ok(Some(pkg)) = self.get_package_by_name(&pkgname) {
264 if let Err(e) = self.store_build_result(pkg.id, &result) {
265 warn!(pkgname = %pkgname, error = %e, "Failed to migrate build result");
266 }
267 build_count += 1;
268 }
269 }
270
271 self.conn.execute_batch(
273 "DROP TABLE IF EXISTS scan;
274 DROP TABLE IF EXISTS build;",
275 )?;
276
277 info!(
278 packages = migrated_count,
279 builds = build_count,
280 "Migration to v2 complete"
281 );
282 Ok(())
283 }
284
285 pub fn store_package(
291 &self,
292 pkgpath: &str,
293 index: &ScanIndex,
294 ) -> Result<i64> {
295 let pkgname = index.pkgname.pkgname();
296 let (base, version) = split_pkgname(pkgname);
297
298 let skip_reason =
299 index.pkg_skip_reason.as_ref().filter(|s| !s.is_empty());
300 let fail_reason =
301 index.pkg_fail_reason.as_ref().filter(|s| !s.is_empty());
302 let is_bootstrap = index.bootstrap_pkg.as_deref() == Some("yes");
303 let pbulk_weight: i32 = index
304 .pbulk_weight
305 .as_ref()
306 .and_then(|s| s.parse().ok())
307 .unwrap_or(100);
308
309 let scan_data = serde_json::to_string(index)?;
310 let now = std::time::SystemTime::now()
311 .duration_since(std::time::UNIX_EPOCH)?
312 .as_secs() as i64;
313
314 self.conn.execute(
315 "INSERT OR REPLACE INTO packages
316 (pkgname, pkgpath, pkgname_base, version, skip_reason, fail_reason,
317 is_bootstrap, pbulk_weight, scan_data, scanned_at)
318 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
319 params![
320 pkgname,
321 pkgpath,
322 base,
323 version,
324 skip_reason,
325 fail_reason,
326 is_bootstrap,
327 pbulk_weight,
328 scan_data,
329 now
330 ],
331 )?;
332
333 let package_id = self.conn.last_insert_rowid();
334
335 if let Some(ref deps) = index.all_depends {
337 for dep in deps {
338 self.conn.execute(
339 "INSERT OR IGNORE INTO depends (package_id, depend_pattern, depend_pkgpath)
340 VALUES (?1, ?2, ?3)",
341 params![package_id, dep.pattern().pattern(), dep.pkgpath().to_string()],
342 )?;
343 }
344 }
345
346 debug!(pkgname = pkgname, package_id = package_id, "Stored package");
347 Ok(package_id)
348 }
349
350 pub fn store_scan_pkgpath(
352 &self,
353 pkgpath: &str,
354 indexes: &[ScanIndex],
355 ) -> Result<()> {
356 for index in indexes {
357 self.store_package(pkgpath, index)?;
358 }
359 Ok(())
360 }
361
362 pub fn get_package_by_name(
364 &self,
365 pkgname: &str,
366 ) -> Result<Option<PackageRow>> {
367 let result = self.conn.query_row(
368 "SELECT id, pkgname, pkgpath, skip_reason, fail_reason, is_bootstrap, pbulk_weight
369 FROM packages WHERE pkgname = ?1",
370 [pkgname],
371 |row| Ok(PackageRow {
372 id: row.get(0)?,
373 pkgname: row.get(1)?,
374 pkgpath: row.get(2)?,
375 skip_reason: row.get(3)?,
376 fail_reason: row.get(4)?,
377 is_bootstrap: row.get::<_, i32>(5)? != 0,
378 pbulk_weight: row.get(6)?,
379 }),
380 );
381
382 match result {
383 Ok(pkg) => Ok(Some(pkg)),
384 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
385 Err(e) => Err(e.into()),
386 }
387 }
388
389 pub fn get_package_id(&self, pkgname: &str) -> Result<Option<i64>> {
391 let result = self.conn.query_row(
392 "SELECT id FROM packages WHERE pkgname = ?1",
393 [pkgname],
394 |row| row.get(0),
395 );
396
397 match result {
398 Ok(id) => Ok(Some(id)),
399 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
400 Err(e) => Err(e.into()),
401 }
402 }
403
404 pub fn get_pkgname(&self, package_id: i64) -> Result<String> {
406 self.conn
407 .query_row(
408 "SELECT pkgname FROM packages WHERE id = ?1",
409 [package_id],
410 |row| row.get(0),
411 )
412 .context("Package not found")
413 }
414
415 pub fn get_packages_by_path(
417 &self,
418 pkgpath: &str,
419 ) -> Result<Vec<PackageRow>> {
420 let mut stmt = self.conn.prepare(
421 "SELECT id, pkgname, pkgpath, skip_reason, fail_reason, is_bootstrap, pbulk_weight
422 FROM packages WHERE pkgpath = ?1"
423 )?;
424
425 let rows = stmt.query_map([pkgpath], |row| {
426 Ok(PackageRow {
427 id: row.get(0)?,
428 pkgname: row.get(1)?,
429 pkgpath: row.get(2)?,
430 skip_reason: row.get(3)?,
431 fail_reason: row.get(4)?,
432 is_bootstrap: row.get::<_, i32>(5)? != 0,
433 pbulk_weight: row.get(6)?,
434 })
435 })?;
436
437 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
438 }
439
440 pub fn is_pkgpath_scanned(&self, pkgpath: &str) -> Result<bool> {
442 let count: i32 = self.conn.query_row(
443 "SELECT COUNT(*) FROM packages WHERE pkgpath = ?1",
444 [pkgpath],
445 |row| row.get(0),
446 )?;
447 Ok(count > 0)
448 }
449
450 pub fn get_scanned_pkgpaths(&self) -> Result<HashSet<String>> {
452 let mut stmt =
453 self.conn.prepare("SELECT DISTINCT pkgpath FROM packages")?;
454 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
455 rows.collect::<Result<HashSet<_>, _>>().map_err(Into::into)
456 }
457
458 pub fn get_unscanned_dependencies(&self) -> Result<HashSet<String>> {
462 let mut stmt = self.conn.prepare(
463 "SELECT DISTINCT d.depend_pkgpath
464 FROM depends d
465 WHERE d.depend_pkgpath NOT IN (SELECT pkgpath FROM packages)",
466 )?;
467 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
468 rows.collect::<Result<HashSet<_>, _>>().map_err(Into::into)
469 }
470
471 pub fn count_packages(&self) -> Result<i64> {
473 self.conn
474 .query_row("SELECT COUNT(*) FROM packages", [], |row| row.get(0))
475 .context("Failed to count packages")
476 }
477
478 pub fn count_scan(&self) -> Result<i64> {
480 self.conn
481 .query_row(
482 "SELECT COUNT(DISTINCT pkgpath) FROM packages",
483 [],
484 |row| row.get(0),
485 )
486 .context("Failed to count scan")
487 }
488
489 pub fn get_all_packages(&self) -> Result<Vec<PackageRow>> {
491 let mut stmt = self.conn.prepare(
492 "SELECT id, pkgname, pkgpath, skip_reason, fail_reason, is_bootstrap, pbulk_weight
493 FROM packages ORDER BY id"
494 )?;
495
496 let rows = stmt.query_map([], |row| {
497 Ok(PackageRow {
498 id: row.get(0)?,
499 pkgname: row.get(1)?,
500 pkgpath: row.get(2)?,
501 skip_reason: row.get(3)?,
502 fail_reason: row.get(4)?,
503 is_bootstrap: row.get::<_, i32>(5)? != 0,
504 pbulk_weight: row.get(6)?,
505 })
506 })?;
507
508 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
509 }
510
511 pub fn get_buildable_packages(&self) -> Result<Vec<PackageRow>> {
513 let mut stmt = self.conn.prepare(
514 "SELECT id, pkgname, pkgpath, skip_reason, fail_reason, is_bootstrap, pbulk_weight
515 FROM packages WHERE skip_reason IS NULL AND fail_reason IS NULL"
516 )?;
517
518 let rows = stmt.query_map([], |row| {
519 Ok(PackageRow {
520 id: row.get(0)?,
521 pkgname: row.get(1)?,
522 pkgpath: row.get(2)?,
523 skip_reason: row.get(3)?,
524 fail_reason: row.get(4)?,
525 is_bootstrap: row.get::<_, i32>(5)? != 0,
526 pbulk_weight: row.get(6)?,
527 })
528 })?;
529
530 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
531 }
532
533 pub fn get_full_scan_index(&self, package_id: i64) -> Result<ScanIndex> {
535 let json: String = self.conn.query_row(
536 "SELECT scan_data FROM packages WHERE id = ?1",
537 [package_id],
538 |row| row.get(0),
539 )?;
540 serde_json::from_str(&json).context("Failed to deserialize scan data")
541 }
542
543 pub fn get_scan_index_by_name(
545 &self,
546 pkgname: &str,
547 ) -> Result<Option<ScanIndex>> {
548 let result = self.conn.query_row(
549 "SELECT scan_data FROM packages WHERE pkgname = ?1",
550 [pkgname],
551 |row| row.get::<_, String>(0),
552 );
553
554 match result {
555 Ok(json) => {
556 let index: ScanIndex = serde_json::from_str(&json)
557 .context("Failed to deserialize scan data")?;
558 Ok(Some(index))
559 }
560 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
561 Err(e) => Err(e.into()),
562 }
563 }
564
565 pub fn clear_scan(&self) -> Result<()> {
567 self.conn.execute("DELETE FROM packages", [])?;
568 self.clear_full_scan_complete()?;
569 self.clear_resolved_depends()?;
570 Ok(())
571 }
572
573 pub fn store_resolved_dependency(
579 &self,
580 package_id: i64,
581 depends_on_id: i64,
582 ) -> Result<()> {
583 self.conn.execute(
584 "INSERT OR IGNORE INTO resolved_depends (package_id, depends_on_id) VALUES (?1, ?2)",
585 params![package_id, depends_on_id],
586 )?;
587 Ok(())
588 }
589
590 pub fn store_resolved_dependencies_batch(
592 &self,
593 deps: &[(i64, i64)],
594 ) -> Result<()> {
595 self.conn.execute("BEGIN TRANSACTION", [])?;
596 for (package_id, depends_on_id) in deps {
597 self.conn.execute(
598 "INSERT OR IGNORE INTO resolved_depends (package_id, depends_on_id) VALUES (?1, ?2)",
599 params![package_id, depends_on_id],
600 )?;
601 }
602 self.conn.execute("COMMIT", [])?;
603 Ok(())
604 }
605
606 pub fn get_dependencies(&self, package_id: i64) -> Result<Vec<i64>> {
608 let mut stmt = self.conn.prepare(
609 "SELECT depends_on_id FROM resolved_depends WHERE package_id = ?1",
610 )?;
611 let rows = stmt.query_map([package_id], |row| row.get::<_, i64>(0))?;
612 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
613 }
614
615 pub fn get_reverse_dependencies(
617 &self,
618 package_id: i64,
619 ) -> Result<Vec<i64>> {
620 let mut stmt = self.conn.prepare(
621 "SELECT package_id FROM resolved_depends WHERE depends_on_id = ?1",
622 )?;
623 let rows = stmt.query_map([package_id], |row| row.get::<_, i64>(0))?;
624 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
625 }
626
627 pub fn get_transitive_reverse_deps(
629 &self,
630 package_id: i64,
631 ) -> Result<Vec<i64>> {
632 let mut stmt = self.conn.prepare(
633 "WITH RECURSIVE affected(id) AS (
634 SELECT ?1
635 UNION
636 SELECT rd.package_id
637 FROM resolved_depends rd
638 JOIN affected a ON rd.depends_on_id = a.id
639 )
640 SELECT id FROM affected WHERE id != ?1",
641 )?;
642 let rows = stmt.query_map([package_id], |row| row.get::<_, i64>(0))?;
643 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
644 }
645
646 pub fn is_resolved(&self) -> Result<bool> {
648 let count: i64 = self.conn.query_row(
649 "SELECT COUNT(*) FROM resolved_depends",
650 [],
651 |row| row.get(0),
652 )?;
653 Ok(count > 0)
654 }
655
656 pub fn clear_resolved_depends(&self) -> Result<()> {
658 self.conn.execute("DELETE FROM resolved_depends", [])?;
659 Ok(())
660 }
661
662 pub fn get_raw_dependencies(
664 &self,
665 package_id: i64,
666 ) -> Result<Vec<(String, String)>> {
667 let mut stmt = self.conn.prepare(
668 "SELECT depend_pattern, depend_pkgpath FROM depends WHERE package_id = ?1"
669 )?;
670 let rows = stmt.query_map([package_id], |row| {
671 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
672 })?;
673 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
674 }
675
676 pub fn store_build_result(
682 &self,
683 package_id: i64,
684 result: &BuildResult,
685 ) -> Result<()> {
686 let (outcome, detail) = build_outcome_to_db(&result.outcome);
687 let duration_ms = result.duration.as_millis() as i64;
688 let now = std::time::SystemTime::now()
689 .duration_since(std::time::UNIX_EPOCH)?
690 .as_secs() as i64;
691 let log_dir = result.log_dir.as_ref().map(|p| p.display().to_string());
692
693 self.conn.execute(
694 "INSERT OR REPLACE INTO builds
695 (package_id, outcome, outcome_detail, duration_ms, built_at, log_dir)
696 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
697 params![package_id, outcome, detail, duration_ms, now, log_dir],
698 )?;
699
700 debug!(
701 package_id = package_id,
702 outcome = outcome,
703 "Stored build result"
704 );
705 Ok(())
706 }
707
708 pub fn store_build_by_name(&self, result: &BuildResult) -> Result<()> {
710 if let Some(pkg) = self.get_package_by_name(result.pkgname.pkgname())? {
711 self.store_build_result(pkg.id, result)
712 } else {
713 warn!(pkgname = %result.pkgname.pkgname(), "Package not found in database for build result");
714 Ok(())
715 }
716 }
717
718 pub fn store_build_batch(&self, results: &[BuildResult]) -> Result<()> {
720 self.conn.execute("BEGIN TRANSACTION", [])?;
721 for result in results {
722 if let Err(e) = self.store_build_by_name(result) {
723 let _ = self.conn.execute("ROLLBACK", []);
724 return Err(e);
725 }
726 }
727 self.conn.execute("COMMIT", [])?;
728 debug!(count = results.len(), "Stored build results batch");
729 Ok(())
730 }
731
732 pub fn get_build_result(
734 &self,
735 package_id: i64,
736 ) -> Result<Option<BuildResult>> {
737 let result = self.conn.query_row(
738 "SELECT p.pkgname, p.pkgpath, b.outcome, b.outcome_detail, b.duration_ms, b.log_dir
739 FROM builds b
740 JOIN packages p ON b.package_id = p.id
741 WHERE b.package_id = ?1",
742 [package_id],
743 |row| {
744 let pkgname: String = row.get(0)?;
745 let pkgpath: Option<String> = row.get(1)?;
746 let outcome: String = row.get(2)?;
747 let detail: Option<String> = row.get(3)?;
748 let duration_ms: i64 = row.get(4)?;
749 let log_dir: Option<String> = row.get(5)?;
750 Ok((pkgname, pkgpath, outcome, detail, duration_ms, log_dir))
751 },
752 );
753
754 match result {
755 Ok((pkgname, pkgpath, outcome, detail, duration_ms, log_dir)) => {
756 let build_outcome = db_outcome_to_build(&outcome, detail);
757 Ok(Some(BuildResult {
758 pkgname: PkgName::new(&pkgname),
759 pkgpath: pkgpath.and_then(|p| PkgPath::new(&p).ok()),
760 outcome: build_outcome,
761 duration: Duration::from_millis(duration_ms as u64),
762 log_dir: log_dir.map(std::path::PathBuf::from),
763 }))
764 }
765 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
766 Err(e) => Err(e.into()),
767 }
768 }
769
770 pub fn is_package_complete(&self, package_id: i64) -> Result<bool> {
772 let result = self.conn.query_row(
773 "SELECT outcome FROM builds WHERE package_id = ?1",
774 [package_id],
775 |row| row.get::<_, String>(0),
776 );
777
778 match result {
779 Ok(outcome) => Ok(outcome == "success" || outcome == "up_to_date"),
780 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
781 Err(e) => Err(e.into()),
782 }
783 }
784
785 pub fn is_package_failed(&self, package_id: i64) -> Result<bool> {
787 let result = self.conn.query_row(
788 "SELECT outcome FROM builds WHERE package_id = ?1",
789 [package_id],
790 |row| row.get::<_, String>(0),
791 );
792
793 match result {
794 Ok(outcome) => Ok(outcome != "success" && outcome != "up_to_date"),
795 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
796 Err(e) => Err(e.into()),
797 }
798 }
799
800 pub fn get_completed_package_ids(&self) -> Result<HashSet<i64>> {
802 let mut stmt = self.conn.prepare(
803 "SELECT package_id FROM builds WHERE outcome IN ('success', 'up_to_date')"
804 )?;
805 let rows = stmt.query_map([], |row| row.get::<_, i64>(0))?;
806 rows.collect::<Result<HashSet<_>, _>>().map_err(Into::into)
807 }
808
809 pub fn get_failed_package_ids(&self) -> Result<HashSet<i64>> {
811 let mut stmt = self.conn.prepare(
812 "SELECT package_id FROM builds WHERE outcome NOT IN ('success', 'up_to_date')"
813 )?;
814 let rows = stmt.query_map([], |row| row.get::<_, i64>(0))?;
815 rows.collect::<Result<HashSet<_>, _>>().map_err(Into::into)
816 }
817
818 pub fn count_build(&self) -> Result<i64> {
820 self.conn
821 .query_row("SELECT COUNT(*) FROM builds", [], |row| row.get(0))
822 .context("Failed to count builds")
823 }
824
825 pub fn clear_build(&self) -> Result<()> {
827 self.conn.execute("DELETE FROM builds", [])?;
828 Ok(())
829 }
830
831 pub fn delete_build_by_name(&self, pkgname: &str) -> Result<bool> {
833 let rows = self.conn.execute(
834 "DELETE FROM builds WHERE package_id IN (SELECT id FROM packages WHERE pkgname = ?1)",
835 params![pkgname],
836 )?;
837 Ok(rows > 0)
838 }
839
840 pub fn delete_build_by_pkgpath(&self, pkgpath: &str) -> Result<usize> {
842 let rows = self.conn.execute(
843 "DELETE FROM builds WHERE package_id IN (SELECT id FROM packages WHERE pkgpath = ?1)",
844 params![pkgpath],
845 )?;
846 Ok(rows)
847 }
848
849 pub fn get_all_build_results(&self) -> Result<Vec<BuildResult>> {
851 let mut stmt = self.conn.prepare(
852 "SELECT p.pkgname, p.pkgpath, b.outcome, b.outcome_detail, b.duration_ms, b.log_dir
853 FROM builds b
854 JOIN packages p ON b.package_id = p.id
855 ORDER BY p.pkgname"
856 )?;
857
858 let rows = stmt.query_map([], |row| {
859 let pkgname: String = row.get(0)?;
860 let pkgpath: Option<String> = row.get(1)?;
861 let outcome: String = row.get(2)?;
862 let detail: Option<String> = row.get(3)?;
863 let duration_ms: i64 = row.get(4)?;
864 let log_dir: Option<String> = row.get(5)?;
865 Ok((pkgname, pkgpath, outcome, detail, duration_ms, log_dir))
866 })?;
867
868 let mut results = Vec::new();
869 for row in rows {
870 let (pkgname, pkgpath, outcome, detail, duration_ms, log_dir) =
871 row?;
872 let build_outcome = db_outcome_to_build(&outcome, detail);
873 results.push(BuildResult {
874 pkgname: PkgName::new(&pkgname),
875 pkgpath: pkgpath.and_then(|p| PkgPath::new(&p).ok()),
876 outcome: build_outcome,
877 duration: Duration::from_millis(duration_ms as u64),
878 log_dir: log_dir.map(std::path::PathBuf::from),
879 });
880 }
881
882 Ok(results)
883 }
884
885 pub fn count_breaks_for_failed(
888 &self,
889 ) -> Result<std::collections::HashMap<String, usize>> {
890 use std::collections::HashMap;
891
892 let mut counts: HashMap<String, usize> = HashMap::new();
893
894 let mut stmt = self.conn.prepare(
896 "SELECT p.id, p.pkgname FROM builds b
897 JOIN packages p ON b.package_id = p.id
898 WHERE b.outcome = 'failed'",
899 )?;
900
901 let failed: Vec<(i64, String)> = stmt
902 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
903 .filter_map(|r| r.ok())
904 .collect();
905
906 for (_pkg_id, pkgname) in failed {
908 let count: i64 = self.conn.query_row(
909 "SELECT COUNT(*) FROM builds b
910 JOIN packages p ON b.package_id = p.id
911 WHERE b.outcome = 'indirect_failed'
912 AND b.outcome_detail LIKE ?1",
913 params![format!("%{}", pkgname)],
914 |row| row.get(0),
915 )?;
916 counts.insert(pkgname, count as usize);
917 }
918
919 Ok(counts)
920 }
921
922 pub fn get_total_build_duration(&self) -> Result<Duration> {
924 let total_ms: i64 = self.conn.query_row(
925 "SELECT COALESCE(SUM(duration_ms), 0) FROM builds",
926 [],
927 |row| row.get(0),
928 )?;
929 Ok(Duration::from_millis(total_ms as u64))
930 }
931
932 pub fn mark_failure_cascade(
935 &self,
936 package_id: i64,
937 reason: &str,
938 duration: Duration,
939 ) -> Result<usize> {
940 let now = std::time::SystemTime::now()
941 .duration_since(std::time::UNIX_EPOCH)?
942 .as_secs() as i64;
943
944 let pkgname = self.get_pkgname(package_id)?;
945
946 let mut stmt = self.conn.prepare(
948 "WITH RECURSIVE affected(id, depth) AS (
949 SELECT ?1, 0
950 UNION
951 SELECT rd.package_id, a.depth + 1
952 FROM resolved_depends rd
953 JOIN affected a ON rd.depends_on_id = a.id
954 )
955 SELECT id, depth FROM affected ORDER BY depth",
956 )?;
957
958 let affected: Vec<(i64, i32)> = stmt
959 .query_map([package_id], |row| {
960 Ok((row.get::<_, i64>(0)?, row.get::<_, i32>(1)?))
961 })?
962 .filter_map(|r| r.ok())
963 .collect();
964
965 self.conn.execute("BEGIN TRANSACTION", [])?;
967
968 for (id, depth) in &affected {
969 let (outcome, detail, dur) = if *depth == 0 {
970 ("failed", reason.to_string(), duration.as_millis() as i64)
971 } else {
972 ("indirect_failed", format!("depends on failed {}", pkgname), 0)
973 };
974
975 self.conn.execute(
976 "INSERT OR REPLACE INTO builds
977 (package_id, outcome, outcome_detail, duration_ms, built_at)
978 VALUES (?1, ?2, ?3, ?4, ?5)",
979 params![id, outcome, detail, dur, now],
980 )?;
981 }
982
983 self.conn.execute("COMMIT", [])?;
984
985 debug!(
986 package_id = package_id,
987 affected_count = affected.len(),
988 "Marked failure cascade"
989 );
990 Ok(affected.len())
991 }
992
993 pub fn full_scan_complete(&self) -> bool {
999 self.conn
1000 .query_row(
1001 "SELECT value FROM metadata WHERE key = 'full_scan_complete'",
1002 [],
1003 |row| row.get::<_, String>(0),
1004 )
1005 .map(|v| v == "true")
1006 .unwrap_or(false)
1007 }
1008
1009 pub fn set_full_scan_complete(&self) -> Result<()> {
1011 self.conn.execute(
1012 "INSERT OR REPLACE INTO metadata (key, value) VALUES ('full_scan_complete', 'true')",
1013 [],
1014 )?;
1015 Ok(())
1016 }
1017
1018 pub fn clear_full_scan_complete(&self) -> Result<()> {
1020 self.conn.execute(
1021 "DELETE FROM metadata WHERE key = 'full_scan_complete'",
1022 [],
1023 )?;
1024 Ok(())
1025 }
1026
1027 pub fn get_buildable_count(&self) -> Result<i64> {
1029 self.conn.query_row(
1030 "SELECT COUNT(*) FROM packages WHERE skip_reason IS NULL AND fail_reason IS NULL",
1031 [],
1032 |row| row.get(0),
1033 ).context("Failed to count buildable packages")
1034 }
1035
1036 pub fn compare_pkgpath_lists(
1042 &self,
1043 requested: &[&str],
1044 ) -> Result<(Vec<String>, Vec<String>, Vec<String>)> {
1045 let scanned = self.get_scanned_pkgpaths()?;
1046 let requested_set: HashSet<_> =
1047 requested.iter().map(|s| s.to_string()).collect();
1048
1049 let to_add: Vec<_> =
1050 requested_set.difference(&scanned).cloned().collect();
1051 let to_remove: Vec<_> =
1052 scanned.difference(&requested_set).cloned().collect();
1053 let unchanged: Vec<_> =
1054 scanned.intersection(&requested_set).cloned().collect();
1055
1056 Ok((to_add, to_remove, unchanged))
1057 }
1058
1059 pub fn delete_pkgpaths(&self, pkgpaths: &[&str]) -> Result<usize> {
1061 if pkgpaths.is_empty() {
1062 return Ok(0);
1063 }
1064
1065 let mut count = 0;
1066 for pkgpath in pkgpaths {
1067 count += self.conn.execute(
1068 "DELETE FROM packages WHERE pkgpath = ?1",
1069 [pkgpath],
1070 )?;
1071 }
1072 Ok(count)
1073 }
1074}
1075
1076fn split_pkgname(pkgname: &str) -> (String, String) {
1082 let bytes = pkgname.as_bytes();
1084 for i in (0..bytes.len()).rev() {
1085 if bytes[i] == b'-'
1086 && i + 1 < bytes.len()
1087 && bytes[i + 1].is_ascii_digit()
1088 {
1089 return (pkgname[..i].to_string(), pkgname[i + 1..].to_string());
1090 }
1091 }
1092 (pkgname.to_string(), String::new())
1094}
1095
1096fn build_outcome_to_db(
1098 outcome: &BuildOutcome,
1099) -> (&'static str, Option<String>) {
1100 match outcome {
1101 BuildOutcome::Success => ("success", None),
1102 BuildOutcome::UpToDate => ("up_to_date", None),
1103 BuildOutcome::Failed(s) => ("failed", Some(s.clone())),
1104 BuildOutcome::PreFailed(s) => ("pre_failed", Some(s.clone())),
1105 BuildOutcome::IndirectFailed(s) => ("indirect_failed", Some(s.clone())),
1106 BuildOutcome::IndirectPreFailed(s) => {
1107 ("indirect_pre_failed", Some(s.clone()))
1108 }
1109 }
1110}
1111
1112fn db_outcome_to_build(outcome: &str, detail: Option<String>) -> BuildOutcome {
1114 match outcome {
1115 "success" => BuildOutcome::Success,
1116 "up_to_date" => BuildOutcome::UpToDate,
1117 "failed" => BuildOutcome::Failed(detail.unwrap_or_default()),
1118 "pre_failed" => BuildOutcome::PreFailed(detail.unwrap_or_default()),
1119 "indirect_failed" => {
1120 BuildOutcome::IndirectFailed(detail.unwrap_or_default())
1121 }
1122 "indirect_pre_failed" => {
1123 BuildOutcome::IndirectPreFailed(detail.unwrap_or_default())
1124 }
1125 _ => BuildOutcome::Failed(format!("Unknown outcome: {}", outcome)),
1126 }
1127}