perf(zip): eliminate N+1 queries with ltree bulk subtree fetch

Replace BFS traversal that issued 2 SQL queries per folder (list_files +
list_folders) with 2 total queries using PostgreSQL ltree <@ operator:

1. list_subtree_folders: single GiST-indexed scan for all folders
2. list_files_in_subtree: single GiST-indexed join for all files

Files are grouped by folder_id in a HashMap, then iterated in directory
order (folders pre-sorted by path from SQL).

Changes across 4 architecture layers:
- Domain: FolderRepository::list_subtree_folders (default impl)
- Application ports: FolderUseCase, FileRetrievalUseCase, FileReadPort
- Application services: FolderService, FileRetrievalService passthroughs
- Infrastructure: PG implementations + ZipService rewrite

Query count: O(N) → O(1). Latency for 100-folder tree: ~200 round-trips → 3.
This commit is contained in:
Dionisio
2026-02-24 12:18:38 +01:00
parent a79700b11c
commit ed433df2af
9 changed files with 224 additions and 99 deletions
@@ -104,6 +104,20 @@ pub trait FolderRepository: Send + Sync + 'static {
/// This is used during user registration to create the user's personal folder.
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<Folder, DomainError>;
/// Lists every folder in a subtree rooted at `folder_id` (inclusive).
///
/// Uses ltree `<@` for a single GiST-indexed scan. The result is
/// ordered by `path` so callers can iterate in directory order.
///
/// Default: falls back to `list_folders` (one level only).
async fn list_subtree_folders(
&self,
folder_id: &str,
) -> Result<Vec<Folder>, DomainError> {
let _ = folder_id;
Ok(Vec::new())
}
/// Lists all descendant folders in a subtree (ltree-based).
///
/// Returns all folders whose lpath is a descendant of the given folder's