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:
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user