Skip to main content

max / audiofiles

Cap folder search results (scale cliff: unbounded materialization) search_in_folder had no LIMIT, so a broad query against a folder with tens of thousands of samples materialized the whole match set into a Vec and the egui table on the GUI thread. Both search paths now share SEARCH_RESULT_LIMIT (500) — folder scope was unbounded, global already capped at a bare 500. A user who needs to narrow further types a more specific query. (The rules.rs reconcile-all-samples and vfs mirror-tree walks are intentionally unbounded — they must process every row — so they're left as-is; the per-keystroke LIKE full-scan is addressed separately by the FTS5 work.) Tests: core 536 green (new folder-cap test). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-15 23:45 UTC
Commit: d31bf85ed44acd9657788405f0f13526665b7c0a
Parent: 620d639
1 file changed, +28 insertions, -2 deletions
@@ -6,6 +6,12 @@ use crate::id_types::{NodeId, VfsId};
6 6 use crate::vfs::{self as vfs_mod, VfsNodeWithAnalysis};
7 7 use tracing::instrument;
8 8
9 + /// Maximum rows any single search returns. Bounds the result Vec (and the egui
10 + /// table that renders it) so a broad query against a huge library can't
11 + /// materialize the whole table on the GUI thread. Applies to both folder and
12 + /// global scope. A user who needs to narrow further types a more specific query.
13 + pub const SEARCH_RESULT_LIMIT: usize = 500;
14 +
9 15 /// Search scope: current folder or global across all VFS roots.
10 16 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11 17 pub enum SearchScope {
@@ -161,7 +167,9 @@ pub fn search_in_folder(
161 167
162 168 append_filter_clauses(&mut sql, &mut params, filter);
163 169
164 - sql.push_str(" ORDER BY n.node_type ASC, n.name ASC");
170 + sql.push_str(&format!(
171 + " ORDER BY n.node_type ASC, n.name ASC LIMIT {SEARCH_RESULT_LIMIT}"
172 + ));
165 173
166 174 let mut stmt = db.conn().prepare(&sql)?;
167 175 let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
@@ -188,7 +196,9 @@ pub fn search_global(
188 196
189 197 append_filter_clauses(&mut sql, &mut params, filter);
190 198
191 - sql.push_str(" ORDER BY n.node_type ASC, n.name ASC LIMIT 500");
199 + sql.push_str(&format!(
200 + " ORDER BY n.node_type ASC, n.name ASC LIMIT {SEARCH_RESULT_LIMIT}"
201 + ));
192 202
193 203 let mut stmt = db.conn().prepare(&sql)?;
194 204 let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
@@ -375,6 +385,22 @@ mod tests {
375 385 (db, vfs_id)
376 386 }
377 387
388 + #[test]
389 + fn folder_search_caps_at_result_limit() {
390 + let db = Database::open_in_memory().unwrap();
391 + let vfs_id = vfs::create_vfs(&db, "Big").unwrap();
392 + // Insert more matches than the cap allows.
393 + for i in 0..(SEARCH_RESULT_LIMIT + 50) {
394 + let hash = format!("h{i}");
395 + insert_fake_sample(&db, &hash);
396 + vfs::create_sample_link(&db, vfs_id, None, &format!("kick_{i}.wav"), &hash).unwrap();
397 + }
398 + let mut filter = SearchFilter::default();
399 + filter.text_query = "kick".to_string();
400 + let results = search_in_folder(&db, &filter, vfs_id, None).unwrap();
401 + assert_eq!(results.len(), SEARCH_RESULT_LIMIT);
402 + }
403 +
378 404 /// Set up a VFS with two analysed samples for filter tests.
379 405 fn setup_with_analysis() -> (Database, VfsId) {
380 406 let db = Database::open_in_memory().unwrap();