feat(domain): package read queries with SQL-aggregated stats

Merged
MP
6mo ago
+2109-31

Summary

Implements CQRS read-side queries for Package aggregates with SQL-level aggregation (COUNT, SUM, AVG) of member downloads. Unblocks the Package UI (task 29). Fully tested: 16 new integration tests, all acceptance criteria met.

Why

Packages need pre-computed statistics (download count, total size, progress %) without N+1 query overhead. CQRS separation: PackageReadRepository returns flattened DTOs from a single GROUP BY query, never exposes mutation. Matches established pattern from Accounts queries.

Changes

  • Add PackageView and PackageFilter domain models (domain/model/views.rs)
  • Add PackageReadRepository port trait (domain/ports/driven/package_read_repository.rs) with three methods:
    • find_packages(filter) — lists with optional source_type and name_q (fuzzy) filters
    • find_package_by_id(id) — returns single package or None
    • find_package_downloads(id) — returns member downloads ordered by queue_position
  • Implement SqlitePackageReadRepo adapter (adapters/driven/sqlite/package_read_repo.rs):
    • PACKAGE_AGG_SELECT constant: single LEFT JOIN ... GROUP BY query with stats aggregation
    • row_to_view() for SQL→domain mapping (16 integration tests verify correctness)
    • Handles NULL aggregates correctly (COALESCE for SUM/COUNT edge cases)
  • Add three Query handlers: ListPackagesQuery, GetPackageQuery, ListPackageDownloadsQuery
  • Add PackageViewDto read model with #[serde(rename_all = "camelCase")] for frontend
  • Wire in QueryBus and expose three Tauri IPC commands: package_list, package_get, package_list_downloads
  • Update CHANGELOG.md

Testing

All acceptance criteria verified:

  • N+1 prevention: aggregation at SQL level via LEFT JOIN ... GROUP BY p.id, not N+1 application queries. Tested by test_find_packages_aggregates_member_downloads.
  • Fuzzy name filter: LOWER(p.name) LIKE ? is case-insensitive substring match. Verified by test_find_packages_filter_by_name_q_is_case_insensitive_substring.
  • SQLite integration: 16 tests via setup_test_db() + fixtures, no mocks. All pass locally and in CI.
cargo test package_read_repo -- --nocapture
cargo test --workspace

Full test output: 1299 cargo tests pass, 605 vitest pass, clippy clean, cargo fmt clean, oxlint clean.

Related Issues

  • Closes task 28 (Vortex sprint roadmap)
  • Unblocks task 29 (Vue Packages React, all dependencies now satisfied)

Checklist

  • Tests added/updated and passing locally
  • Docs updated (CHANGELOG.md)
  • No secrets, debug prints, or commented-out code
  • Self-reviewed the diff
  • CI green

Summary by cubic

Add read-side package queries with SQL-aggregated stats to avoid N+1 and unblock the Packages UI (task 28). Also clamps progress, scales large member lookups, suppresses ETAs on Completed, and fixes legacy created_at fallback in package member views.

  • New Features

    • New PackageReadRepository and SqlitePackageReadRepo compute downloads_count, total_bytes, downloaded_bytes, progress_percent, and all_completed via one LEFT JOIN ... GROUP BY.
    • Query handlers ListPackagesQuery, GetPackageQuery, ListPackageDownloadsQuery wired into QueryBus; DTO PackageViewDto (camelCase, no password). IPC: package_list, package_get, package_list_downloads; list results ordered by (created_at, id) and member downloads by queue_position.
    • Filtering: source_type exact match; name_q is a Unicode-aware, case-insensitive substring in Rust; both AND together; blank name_q ignored.
  • Bug Fixes

    • Aggregate credits Completed members at full total_bytes (fallback to downloaded_bytes when total is NULL) symmetrically in numerator and denominator.
    • Clamp progress_percent to 100% when downloaded_bytes > total_bytes for non-completed rows (aggregate and per-download views).
    • Chunk member ID and segment lookups in find_package_downloads to stay under SQLite’s variable limit for large packages.
    • Suppress eta_seconds for Completed downloads.
    • Resolve legacy member created_at with a fallback chain (id-inferred → updated_at → sentinel) via resolve_download_created_at, used in both package_read_repo and download_read_repo to avoid “1970” dates and keep ordering consistent.

Summary by CodeRabbit

  • New Features
    • Package browsing: list packages (filter by source type and name substring), deterministic ordering, package details with aggregated download stats/progress, and list package downloads via new IPC endpoints.
    • Accounts: quota-driven account rotation with selector exclusions and related domain/events; account UI and persistence added.
  • Tests
    • Extensive tests for package aggregation, progress clamping, filtering, ordering, DTO mapping, and query handlers.
  • Documentation
    • CHANGELOG updated to document these additions.