fix: defensive hardening — lock release logging, SQLite param guard, vector cast

Three defensive improvements found via peer code review:

1. lock.rs: Lock release errors were silently discarded with `let _ =`.
   If the DELETE failed (disk full, corruption), the lock stayed in the
   database with no diagnostic. Next sync would require --force with no
   clue why. Now logs with error!() including the underlying error message.

2. filters.rs: Dynamic SQL label filter construction had no upper bound
   on bind parameters. With many combined filters, param_idx + labels.len()
   could exceed SQLite's 999-parameter limit, producing an opaque error.
   Added a guard that caps labels at 900 - param_idx.

3. vector.rs: max_chunks_per_document returned i64 which was cast to
   usize. A negative value from a corrupt database would wrap to a huge
   number, causing overflow in the multiplier calculation. Now clamped
   to .max(1) and cast via unsigned_abs().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-08 07:55:54 -05:00
parent d3306114eb
commit 5786d7f4b6
3 changed files with 21 additions and 9 deletions

View File

@@ -121,12 +121,17 @@ impl AppLock {
let _ = handle.join(); let _ = handle.join();
} }
let _ = self.conn.execute( match self.conn.execute(
"DELETE FROM app_locks WHERE name = ? AND owner = ?", "DELETE FROM app_locks WHERE name = ? AND owner = ?",
(&self.name, &self.owner), (&self.name, &self.owner),
); ) {
Ok(_) => info!(owner = %self.owner, "Lock released"),
info!(owner = %self.owner, "Lock released"); Err(e) => error!(
owner = %self.owner,
error = %e,
"Failed to release lock; may require --force on next sync"
),
}
} }
fn start_heartbeat(&mut self) { fn start_heartbeat(&mut self) {

View File

@@ -98,15 +98,22 @@ pub fn apply_filters(
} }
if !filters.labels.is_empty() { if !filters.labels.is_empty() {
let placeholders: Vec<String> = (0..filters.labels.len()) // SQLite has a default limit of 999 bind parameters.
let max_labels = 900_usize.saturating_sub(param_idx);
let label_slice = if filters.labels.len() > max_labels {
&filters.labels[..max_labels]
} else {
&filters.labels
};
let placeholders: Vec<String> = (0..label_slice.len())
.map(|i| format!("?{}", param_idx + i)) .map(|i| format!("?{}", param_idx + i))
.collect(); .collect();
sql.push_str(&format!( sql.push_str(&format!(
" AND EXISTS (SELECT 1 FROM document_labels dl WHERE dl.document_id = d.id AND dl.label_name IN ({}) GROUP BY dl.document_id HAVING COUNT(DISTINCT dl.label_name) = {})", " AND EXISTS (SELECT 1 FROM document_labels dl WHERE dl.document_id = d.id AND dl.label_name IN ({}) GROUP BY dl.document_id HAVING COUNT(DISTINCT dl.label_name) = {})",
placeholders.join(","), placeholders.join(","),
filters.labels.len() label_slice.len()
)); ));
for label in &filters.labels { for label in label_slice {
params.push(Box::new(label.clone())); params.push(Box::new(label.clone()));
param_idx += 1; param_idx += 1;
} }

View File

@@ -50,8 +50,8 @@ pub fn search_vector(
.flat_map(|f| f.to_le_bytes()) .flat_map(|f| f.to_le_bytes())
.collect(); .collect();
let max_chunks = max_chunks_per_document(conn); let max_chunks = max_chunks_per_document(conn).max(1);
let multiplier = ((max_chunks as usize * 3 / 2) + 1).max(8); let multiplier = ((max_chunks.unsigned_abs() as usize * 3 / 2) + 1).max(8);
let k = limit * multiplier; let k = limit * multiplier;
let mut stmt = conn.prepare( let mut stmt = conn.prepare(