max / goingson
1 file changed,
+65 insertions,
-103 deletions
| @@ -243,6 +243,63 @@ fn build_is_filter_clauses(is_filters: &[IsFilter]) -> Vec<String> { | |||
| 243 | 243 | .collect() | |
| 244 | 244 | } | |
| 245 | 245 | ||
| 246 | + | /// Append the shared `project:` filters (by ID, then by partial name) to a search | |
| 247 | + | /// query being assembled, binding their parameters and advancing `param_idx`. | |
| 248 | + | /// `alias` is the SQL table alias the `project_id` column hangs off (e.g. `t`, `e`, `ev`). | |
| 249 | + | fn push_project_filters( | |
| 250 | + | sql: &mut String, | |
| 251 | + | params: &mut Vec<String>, | |
| 252 | + | param_idx: &mut i32, | |
| 253 | + | query: &SearchQuery, | |
| 254 | + | alias: &str, | |
| 255 | + | ) { | |
| 256 | + | // Project filter by ID | |
| 257 | + | if let Some(pid) = &query.project_id { | |
| 258 | + | sql.push_str(&format!(" AND {}.project_id = ${}", alias, param_idx)); | |
| 259 | + | params.push(pid.to_string()); | |
| 260 | + | *param_idx += 1; | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // Project filter by name (partial match) | |
| 264 | + | if let Some(pname) = &query.project_name { | |
| 265 | + | sql.push_str(&format!( | |
| 266 | + | " AND {}.project_id IN (SELECT id FROM projects WHERE name LIKE ${} ESCAPE '\\' COLLATE NOCASE)", | |
| 267 | + | alias, param_idx | |
| 268 | + | )); | |
| 269 | + | params.push(format!("%{}%", escape_like(pname))); | |
| 270 | + | *param_idx += 1; | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | /// Run an assembled FTS search: bind the optional MATCH term and `user_id`, then the | |
| 275 | + | /// positional `params`, and fetch typed rows. When `search_term` is `None` the term | |
| 276 | + | /// bind is skipped (the direct, filter-only query path). Every `search_*_fts` helper | |
| 277 | + | /// shares this bind-and-fetch tail; only the row type and result mapping differ. | |
| 278 | + | async fn run_fts_query<R>( | |
| 279 | + | pool: &SqlitePool, | |
| 280 | + | sql: &str, | |
| 281 | + | search_term: Option<&str>, | |
| 282 | + | user_id: &str, | |
| 283 | + | params: Vec<String>, | |
| 284 | + | ) -> Result<Vec<R>> | |
| 285 | + | where | |
| 286 | + | R: for<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow> + Send + Unpin, | |
| 287 | + | { | |
| 288 | + | let mut db_query = sqlx::query_as::<_, R>(sql); | |
| 289 | + | ||
| 290 | + | if let Some(term) = search_term { | |
| 291 | + | db_query = db_query.bind(term).bind(user_id); | |
| 292 | + | } else { | |
| 293 | + | db_query = db_query.bind(user_id); | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | for param in params { | |
| 297 | + | db_query = db_query.bind(param); | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | db_query.fetch_all(pool).await.map_err(CoreError::database) | |
| 301 | + | } | |
| 302 | + | ||
| 246 | 303 | /// Searches tasks via FTS5 MATCH with BM25 relevance ranking, or direct query when | |
| 247 | 304 | /// no search text is provided. Applies optional filters: `is:` status/date, project | |
| 248 | 305 | /// (by ID or name), priority, tag include/exclude, and date range on the due field. | |
| @@ -312,22 +369,7 @@ async fn search_tasks_fts( | |||
| 312 | 369 | sql.push_str(&format!(" AND {}", clause)); | |
| 313 | 370 | } | |
| 314 | 371 | ||
| 315 | - | // Project filter by ID | |
| 316 | - | if let Some(pid) = &query.project_id { | |
| 317 | - | sql.push_str(&format!(" AND t.project_id = ${}", param_idx)); | |
| 318 | - | params.push(pid.to_string()); | |
| 319 | - | param_idx += 1; | |
| 320 | - | } | |
| 321 | - | ||
| 322 | - | // Project filter by name (partial match) | |
| 323 | - | if let Some(pname) = &query.project_name { | |
| 324 | - | sql.push_str(&format!( | |
| 325 | - | " AND t.project_id IN (SELECT id FROM projects WHERE name LIKE ${} ESCAPE '\\' COLLATE NOCASE)", | |
| 326 | - | param_idx | |
| 327 | - | )); | |
| 328 | - | params.push(format!("%{}%", escape_like(pname))); | |
| 329 | - | param_idx += 1; | |
| 330 | - | } | |
| 372 | + | push_project_filters(&mut sql, &mut params, &mut param_idx, query, "t"); | |
| 331 | 373 | ||
| 332 | 374 | // Priority filter | |
| 333 | 375 | if let Some(priority) = &query.priority { | |
| @@ -377,22 +419,7 @@ async fn search_tasks_fts( | |||
| 377 | 419 | ||
| 378 | 420 | sql.push_str(&format!(" ORDER BY rank LIMIT {}", per_type_limit)); | |
| 379 | 421 | ||
| 380 | - | // Build and execute query | |
| 381 | - | let mut db_query = sqlx::query_as::<_, Row>(&sql); | |
| 382 | - | ||
| 383 | - | if let Some(term) = search_term { | |
| 384 | - | db_query = db_query | |
| 385 | - | .bind(term) | |
| 386 | - | .bind(user_id); | |
| 387 | - | } else { | |
| 388 | - | db_query = db_query.bind(user_id); | |
| 389 | - | } | |
| 390 | - | ||
| 391 | - | for param in params { | |
| 392 | - | db_query = db_query.bind(param); | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | let rows: Vec<Row> = db_query.fetch_all(pool).await.map_err(CoreError::database)?; | |
| 422 | + | let rows: Vec<Row> = run_fts_query(pool, &sql, search_term, user_id, params).await?; | |
| 396 | 423 | ||
| 397 | 424 | rows.into_iter() | |
| 398 | 425 | .map(|row| { | |
| @@ -459,22 +486,7 @@ async fn search_emails_fts( | |||
| 459 | 486 | let mut params: Vec<String> = Vec::new(); | |
| 460 | 487 | let mut param_idx = 3; | |
| 461 | 488 | ||
| 462 | - | // Project filter by ID | |
| 463 | - | if let Some(pid) = &query.project_id { | |
| 464 | - | sql.push_str(&format!(" AND e.project_id = ${}", param_idx)); | |
| 465 | - | params.push(pid.to_string()); | |
| 466 | - | param_idx += 1; | |
| 467 | - | } | |
| 468 | - | ||
| 469 | - | // Project filter by name | |
| 470 | - | if let Some(pname) = &query.project_name { | |
| 471 | - | sql.push_str(&format!( | |
| 472 | - | " AND e.project_id IN (SELECT id FROM projects WHERE name LIKE ${} ESCAPE '\\' COLLATE NOCASE)", | |
| 473 | - | param_idx | |
| 474 | - | )); | |
| 475 | - | params.push(format!("%{}%", escape_like(pname))); | |
| 476 | - | param_idx += 1; | |
| 477 | - | } | |
| 489 | + | push_project_filters(&mut sql, &mut params, &mut param_idx, query, "e"); | |
| 478 | 490 | ||
| 479 | 491 | // Date filters. Compare the stored `received_at` text directly (it is written | |
| 480 | 492 | // in the sortable "%Y-%m-%d %H:%M:%S" format, so lexicographic order is | |
| @@ -494,15 +506,7 @@ async fn search_emails_fts( | |||
| 494 | 506 | ||
| 495 | 507 | sql.push_str(&format!(" ORDER BY rank LIMIT {}", per_type_limit)); | |
| 496 | 508 | ||
| 497 | - | let mut db_query = sqlx::query_as::<_, Row>(&sql) | |
| 498 | - | .bind(search_term) | |
| 499 | - | .bind(user_id); | |
| 500 | - | ||
| 501 | - | for param in params { | |
| 502 | - | db_query = db_query.bind(param); | |
| 503 | - | } | |
| 504 | - | ||
| 505 | - | let rows: Vec<Row> = db_query.fetch_all(pool).await.map_err(CoreError::database)?; | |
| 509 | + | let rows: Vec<Row> = run_fts_query(pool, &sql, Some(search_term), user_id, params).await?; | |
| 506 | 510 | ||
| 507 | 511 | rows.into_iter() | |
| 508 | 512 | .map(|row| { | |
| @@ -587,15 +591,7 @@ async fn search_projects_fts( | |||
| 587 | 591 | ||
| 588 | 592 | sql.push_str(&format!(" ORDER BY rank LIMIT {}", per_type_limit)); | |
| 589 | 593 | ||
| 590 | - | let mut db_query = sqlx::query_as::<_, Row>(&sql) | |
| 591 | - | .bind(search_term) | |
| 592 | - | .bind(user_id); | |
| 593 | - | ||
| 594 | - | for param in params { | |
| 595 | - | db_query = db_query.bind(param); | |
| 596 | - | } | |
| 597 | - | ||
| 598 | - | let rows: Vec<Row> = db_query.fetch_all(pool).await.map_err(CoreError::database)?; | |
| 594 | + | let rows: Vec<Row> = run_fts_query(pool, &sql, Some(search_term), user_id, params).await?; | |
| 599 | 595 | ||
| 600 | 596 | rows.into_iter() | |
| 601 | 597 | .map(|row| { | |
| @@ -703,22 +699,7 @@ async fn search_events_fts( | |||
| 703 | 699 | } | |
| 704 | 700 | } | |
| 705 | 701 | ||
| 706 | - | // Project filter by ID | |
| 707 | - | if let Some(pid) = &query.project_id { | |
| 708 | - | sql.push_str(&format!(" AND ev.project_id = ${}", param_idx)); | |
| 709 | - | params.push(pid.to_string()); | |
| 710 | - | param_idx += 1; | |
| 711 | - | } | |
| 712 | - | ||
| 713 | - | // Project filter by name | |
| 714 | - | if let Some(pname) = &query.project_name { | |
| 715 | - | sql.push_str(&format!( | |
| 716 | - | " AND ev.project_id IN (SELECT id FROM projects WHERE name LIKE ${} ESCAPE '\\' COLLATE NOCASE)", | |
| 717 | - | param_idx | |
| 718 | - | )); | |
| 719 | - | params.push(format!("%{}%", escape_like(pname))); | |
| 720 | - | param_idx += 1; | |
| 721 | - | } | |
| 702 | + | push_project_filters(&mut sql, &mut params, &mut param_idx, query, "ev"); | |
| 722 | 703 | ||
| 723 | 704 | // Date filters. Sargable bare-column comparison against a same-format bound. | |
| 724 | 705 | if let Some(df) = &query.date_from { | |
| @@ -739,21 +720,7 @@ async fn search_events_fts( | |||
| 739 | 720 | ||
| 740 | 721 | sql.push_str(&format!(" ORDER BY rank LIMIT {}", per_type_limit)); | |
| 741 | 722 | ||
| 742 | - | let mut db_query = sqlx::query_as::<_, Row>(&sql); | |
| 743 | - | ||
| 744 | - | if let Some(term) = search_term { | |
| 745 | - | db_query = db_query | |
| 746 | - | .bind(term) | |
| 747 | - | .bind(user_id); | |
| 748 | - | } else { | |
| 749 | - | db_query = db_query.bind(user_id); | |
| 750 | - | } | |
| 751 | - | ||
| 752 | - | for param in params { | |
| 753 | - | db_query = db_query.bind(param); | |
| 754 | - | } | |
| 755 | - | ||
| 756 | - | let rows: Vec<Row> = db_query.fetch_all(pool).await.map_err(CoreError::database)?; | |
| 723 | + | let rows: Vec<Row> = run_fts_query(pool, &sql, search_term, user_id, params).await?; | |
| 757 | 724 | ||
| 758 | 725 | rows.into_iter() | |
| 759 | 726 | .map(|row| { | |
| @@ -827,12 +794,7 @@ async fn search_contacts_fts( | |||
| 827 | 794 | LIMIT {} | |
| 828 | 795 | "#, per_type_limit); | |
| 829 | 796 | ||
| 830 | - | let rows: Vec<Row> = sqlx::query_as::<_, Row>(&sql) | |
| 831 | - | .bind(search_term) | |
| 832 | - | .bind(user_id) | |
| 833 | - | .fetch_all(pool) | |
| 834 | - | .await | |
| 835 | - | .map_err(CoreError::database)?; | |
| 797 | + | let rows: Vec<Row> = run_fts_query(pool, &sql, Some(search_term), user_id, Vec::new()).await?; | |
| 836 | 798 | ||
| 837 | 799 | rows.into_iter() | |
| 838 | 800 | .map(|row| { |